mcpick 0.0.14 → 0.0.16

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 CHANGED
@@ -1,5 +1,22 @@
1
1
  # mcpick
2
2
 
3
+ ## 0.0.16
4
+
5
+ ### Patch Changes
6
+
7
+ - b671135: fix: validate marketplace repos upfront, remove unsupported
8
+ --scope flag
9
+ - fed8311: feat: TUI marketplace management, plugin browse, CLI parity
10
+ commands
11
+
12
+ ## 0.0.15
13
+
14
+ ### Patch Changes
15
+
16
+ - 0170ea0: Add local dev workflow: mcpick dev + cache link/unlink
17
+ - 2161258: Fix marketplace refresh, cache clear, uninstall detection;
18
+ add marketplace commands
19
+
3
20
  ## 0.0.14
4
21
 
5
22
  ### Patch Changes
@@ -0,0 +1,60 @@
1
+ import { defineCommand } from 'citty';
2
+ import { mcp_add_json_via_cli } from '../../utils/claude-cli.js';
3
+ import { error, output } from '../output.js';
4
+ export default defineCommand({
5
+ meta: {
6
+ name: 'add-json',
7
+ description: 'Add an MCP server from a JSON configuration string',
8
+ },
9
+ args: {
10
+ name: {
11
+ type: 'positional',
12
+ description: 'Server name',
13
+ required: true,
14
+ },
15
+ config: {
16
+ type: 'positional',
17
+ description: 'JSON configuration string',
18
+ required: true,
19
+ },
20
+ scope: {
21
+ type: 'string',
22
+ description: 'Scope: local, project, or user (default: local)',
23
+ default: 'local',
24
+ },
25
+ json: {
26
+ type: 'boolean',
27
+ description: 'Output as JSON',
28
+ default: false,
29
+ },
30
+ },
31
+ async run({ args }) {
32
+ const scope = args.scope;
33
+ if (!['local', 'project', 'user'].includes(scope)) {
34
+ error(`Invalid scope: ${scope}. Use local, project, or user.`);
35
+ }
36
+ // Validate the JSON is parseable
37
+ try {
38
+ JSON.parse(args.config);
39
+ }
40
+ catch {
41
+ error('Invalid JSON configuration. Provide a valid JSON string.');
42
+ }
43
+ const result = await mcp_add_json_via_cli(args.name, args.config, scope);
44
+ if (args.json) {
45
+ output({
46
+ added: args.name,
47
+ scope,
48
+ success: result.success,
49
+ error: result.error,
50
+ }, true);
51
+ }
52
+ else if (result.success) {
53
+ console.log(`Added '${args.name}' from JSON (scope: ${scope})`);
54
+ }
55
+ else {
56
+ error(result.error || 'Unknown error');
57
+ }
58
+ },
59
+ });
60
+ //# sourceMappingURL=add-json.js.map
@@ -1,5 +1,5 @@
1
1
  import { defineCommand } from 'citty';
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';
2
+ import { clean_orphaned_versions, clear_plugin_caches, get_cached_plugins_info, link_local_plugin, list_linked_plugins, read_installed_plugins, refresh_all_marketplaces, scan_all_cache_keys, unlink_local_plugin, } from '../../core/plugin-cache.js';
3
3
  import { error, output } from '../output.js';
4
4
  const status = defineCommand({
5
5
  meta: {
@@ -65,7 +65,13 @@ const clear = defineCommand({
65
65
  },
66
66
  async run({ args }) {
67
67
  const installed = await read_installed_plugins();
68
- const all_keys = Object.keys(installed.plugins);
68
+ const installed_keys = Object.keys(installed.plugins);
69
+ // When --all, also scan disk for marketplace-sourced caches
70
+ // not tracked in installed_plugins.json
71
+ const scanned_keys = args.all ? await scan_all_cache_keys() : [];
72
+ const all_keys = [
73
+ ...new Set([...installed_keys, ...scanned_keys]),
74
+ ];
69
75
  if (all_keys.length === 0) {
70
76
  if (args.json) {
71
77
  output({ cleared: [], errors: [] }, true);
@@ -77,7 +83,8 @@ const clear = defineCommand({
77
83
  }
78
84
  let keys_to_clear;
79
85
  if (args.plugin) {
80
- if (!installed.plugins[args.plugin]) {
86
+ if (!installed.plugins[args.plugin] &&
87
+ !scanned_keys.includes(args.plugin)) {
81
88
  error(`Plugin '${args.plugin}' not found in cache. Run 'mcpick cache status' to see cached plugins.`);
82
89
  }
83
90
  keys_to_clear = [args.plugin];
@@ -36,9 +36,7 @@ const apply = defineCommand({
36
36
  if (scope && !['local', 'project', 'user'].includes(scope)) {
37
37
  error(`Invalid scope: ${scope}. Use local, project, or user.`);
38
38
  }
39
- const cmd_args = args.args
40
- ? args.args.split(',')
41
- : [];
39
+ const cmd_args = args.args ? args.args.split(',') : [];
42
40
  const result = await apply_dev_override(args.name, args.command, cmd_args, scope);
43
41
  if (args.json) {
44
42
  output(result, true);
@@ -82,7 +80,8 @@ const restore = defineCommand({
82
80
  output(result, true);
83
81
  }
84
82
  else {
85
- if (result.restored.length === 0 && result.errors.length === 0) {
83
+ if (result.restored.length === 0 &&
84
+ result.errors.length === 0) {
86
85
  console.log('No dev overrides to restore.');
87
86
  }
88
87
  else {
@@ -0,0 +1,45 @@
1
+ import { defineCommand } from 'citty';
2
+ import { mcp_get_via_cli } from '../../utils/claude-cli.js';
3
+ import { error, output } from '../output.js';
4
+ export default defineCommand({
5
+ meta: {
6
+ name: 'get',
7
+ description: 'Get details about an MCP server',
8
+ },
9
+ args: {
10
+ name: {
11
+ type: 'positional',
12
+ description: 'Server name',
13
+ required: true,
14
+ },
15
+ json: {
16
+ type: 'boolean',
17
+ description: 'Output as JSON',
18
+ default: false,
19
+ },
20
+ },
21
+ async run({ args }) {
22
+ const result = await mcp_get_via_cli(args.name);
23
+ if (args.json) {
24
+ try {
25
+ const parsed = JSON.parse(result.stdout || '{}');
26
+ output(parsed, true);
27
+ }
28
+ catch {
29
+ output({
30
+ name: args.name,
31
+ success: result.success,
32
+ output: result.stdout,
33
+ error: result.error,
34
+ }, true);
35
+ }
36
+ }
37
+ else if (result.success) {
38
+ console.log(result.stdout || 'No details available.');
39
+ }
40
+ else {
41
+ error(result.error || 'Unknown error');
42
+ }
43
+ },
44
+ });
45
+ //# sourceMappingURL=get.js.map
@@ -0,0 +1,203 @@
1
+ import { defineCommand } from 'citty';
2
+ import { read_marketplace_manifest } from '../../core/plugin-cache.js';
3
+ import { marketplace_add_via_cli, marketplace_list_via_cli, marketplace_remove_via_cli, marketplace_update_via_cli, } from '../../utils/claude-cli.js';
4
+ import { error, output } from '../output.js';
5
+ const list = defineCommand({
6
+ meta: {
7
+ name: 'list',
8
+ description: 'List configured marketplaces',
9
+ },
10
+ args: {
11
+ json: {
12
+ type: 'boolean',
13
+ description: 'Output as JSON',
14
+ default: false,
15
+ },
16
+ },
17
+ async run({ args }) {
18
+ const result = await marketplace_list_via_cli();
19
+ if (!result.success) {
20
+ error(result.error || 'Unknown error');
21
+ }
22
+ if (args.json) {
23
+ // Try to parse CLI JSON output, fallback to raw
24
+ try {
25
+ const parsed = JSON.parse(result.stdout || '[]');
26
+ output(parsed, true);
27
+ }
28
+ catch {
29
+ output({ marketplaces: result.stdout }, true);
30
+ }
31
+ }
32
+ else {
33
+ console.log(result.stdout || 'No marketplaces configured.');
34
+ }
35
+ },
36
+ });
37
+ const add = defineCommand({
38
+ meta: {
39
+ name: 'add',
40
+ description: 'Add a plugin marketplace (a catalog of installable plugins)',
41
+ },
42
+ args: {
43
+ source: {
44
+ type: 'positional',
45
+ description: 'Marketplace source (GitHub repo, URL, or local path)',
46
+ required: true,
47
+ },
48
+ json: {
49
+ type: 'boolean',
50
+ description: 'Output as JSON',
51
+ default: false,
52
+ },
53
+ },
54
+ async run({ args }) {
55
+ const result = await marketplace_add_via_cli(args.source);
56
+ if (args.json) {
57
+ // Try to include available plugins in JSON output
58
+ let available_plugins = [];
59
+ if (result.success) {
60
+ const manifests = await find_marketplace_plugins(args.source);
61
+ available_plugins = manifests;
62
+ }
63
+ output({
64
+ added: args.source,
65
+ success: result.success,
66
+ error: result.error,
67
+ available_plugins,
68
+ }, true);
69
+ }
70
+ else if (result.success) {
71
+ console.log(`Marketplace added: ${args.source}`);
72
+ await show_available_plugins(args.source);
73
+ }
74
+ else {
75
+ error(result.error || 'Unknown error');
76
+ }
77
+ },
78
+ });
79
+ /**
80
+ * Try to find and display available plugins from a newly added marketplace.
81
+ * The marketplace name in the filesystem may differ from the source string,
82
+ * so we try common derivations.
83
+ */
84
+ async function find_marketplace_plugins(source) {
85
+ // Try the source as-is, then extract repo name from various formats
86
+ const candidates = [];
87
+ // Extract repo name from owner/repo, URLs, etc.
88
+ const repo_match = source.match(/([^/]+?)(?:\.git)?$/);
89
+ if (repo_match) {
90
+ candidates.push(repo_match[1].toLowerCase());
91
+ candidates.push(repo_match[1]);
92
+ }
93
+ // Try the full source as a name
94
+ candidates.push(source);
95
+ for (const name of candidates) {
96
+ const manifest = await read_marketplace_manifest(name);
97
+ if (manifest?.plugins?.length) {
98
+ return manifest.plugins.map((p) => {
99
+ const desc = p.description
100
+ ? ` - ${p.description}`
101
+ : '';
102
+ return `${p.name}${desc}`;
103
+ });
104
+ }
105
+ }
106
+ return [];
107
+ }
108
+ async function show_available_plugins(source) {
109
+ const plugins = await find_marketplace_plugins(source);
110
+ if (plugins.length > 0) {
111
+ console.log(`\nAvailable plugins (${plugins.length}):`);
112
+ for (const p of plugins) {
113
+ console.log(` - ${p}`);
114
+ }
115
+ console.log('\nInstall with: mcpick plugins install <name>@<marketplace>');
116
+ }
117
+ else {
118
+ console.log('\nTo browse and install plugins: mcpick plugins install <name>@<marketplace>');
119
+ }
120
+ }
121
+ const remove = defineCommand({
122
+ meta: {
123
+ name: 'remove',
124
+ description: 'Remove a marketplace',
125
+ },
126
+ args: {
127
+ name: {
128
+ type: 'positional',
129
+ description: 'Marketplace name to remove',
130
+ required: true,
131
+ },
132
+ json: {
133
+ type: 'boolean',
134
+ description: 'Output as JSON',
135
+ default: false,
136
+ },
137
+ },
138
+ async run({ args }) {
139
+ const result = await marketplace_remove_via_cli(args.name);
140
+ if (args.json) {
141
+ output({
142
+ removed: args.name,
143
+ success: result.success,
144
+ error: result.error,
145
+ }, true);
146
+ }
147
+ else if (result.success) {
148
+ console.log(`Marketplace removed: ${args.name}`);
149
+ }
150
+ else {
151
+ error(result.error || 'Unknown error');
152
+ }
153
+ },
154
+ });
155
+ const update = defineCommand({
156
+ meta: {
157
+ name: 'update',
158
+ description: 'Update marketplace(s) from source',
159
+ },
160
+ args: {
161
+ name: {
162
+ type: 'positional',
163
+ description: 'Marketplace name to update (omit to update all)',
164
+ required: false,
165
+ },
166
+ json: {
167
+ type: 'boolean',
168
+ description: 'Output as JSON',
169
+ default: false,
170
+ },
171
+ },
172
+ async run({ args }) {
173
+ const result = await marketplace_update_via_cli(args.name || undefined);
174
+ if (args.json) {
175
+ output({
176
+ updated: args.name || 'all',
177
+ success: result.success,
178
+ error: result.error,
179
+ }, true);
180
+ }
181
+ else if (result.success) {
182
+ console.log(args.name
183
+ ? `Marketplace updated: ${args.name}`
184
+ : 'All marketplaces updated.');
185
+ }
186
+ else {
187
+ error(result.error || 'Unknown error');
188
+ }
189
+ },
190
+ });
191
+ export default defineCommand({
192
+ meta: {
193
+ name: 'marketplace',
194
+ description: 'Manage plugin marketplaces (catalogs of installable plugins). Add a marketplace first, then install plugins from it with: mcpick plugins install <name>@<marketplace>',
195
+ },
196
+ subCommands: {
197
+ list,
198
+ add,
199
+ remove,
200
+ update,
201
+ },
202
+ });
203
+ //# sourceMappingURL=marketplace.js.map
@@ -1,6 +1,7 @@
1
1
  import { defineCommand } from 'citty';
2
+ import { parse_plugin_key, read_known_marketplaces, } from '../../core/plugin-cache.js';
2
3
  import { build_enabled_plugins, get_all_plugins, read_claude_settings, write_claude_settings, } from '../../core/settings.js';
3
- import { install_plugin_via_cli, uninstall_plugin_via_cli, update_plugin_via_cli, } from '../../utils/claude-cli.js';
4
+ import { install_plugin_via_cli, uninstall_plugin_via_cli, update_plugin_via_cli, validate_plugin_via_cli, } from '../../utils/claude-cli.js';
4
5
  import { error, output } from '../output.js';
5
6
  const list = defineCommand({
6
7
  meta: {
@@ -144,6 +145,25 @@ const uninstall = defineCommand({
144
145
  },
145
146
  async run({ args }) {
146
147
  const scope = args.scope;
148
+ // Check if this is a marketplace-managed plugin
149
+ const { marketplace } = parse_plugin_key(args.plugin);
150
+ const known = await read_known_marketplaces();
151
+ if (known[marketplace]) {
152
+ const msg = `'${args.plugin}' is managed by marketplace '${marketplace}'.\n` +
153
+ `To remove the marketplace: mcpick marketplace remove ${marketplace}\n` +
154
+ `Or via Claude CLI: claude plugin marketplace remove ${marketplace}`;
155
+ if (args.json) {
156
+ output({
157
+ success: false,
158
+ error: msg,
159
+ marketplace_managed: true,
160
+ }, true);
161
+ }
162
+ else {
163
+ error(msg);
164
+ }
165
+ return;
166
+ }
147
167
  const result = await uninstall_plugin_via_cli(args.plugin, scope);
148
168
  if (args.json) {
149
169
  output(result, true);
@@ -192,11 +212,54 @@ const update = defineCommand({
192
212
  }
193
213
  },
194
214
  });
215
+ const validate = defineCommand({
216
+ meta: {
217
+ name: 'validate',
218
+ description: 'Validate a plugin or marketplace manifest',
219
+ },
220
+ args: {
221
+ path: {
222
+ type: 'positional',
223
+ description: 'Path to plugin or marketplace manifest',
224
+ required: true,
225
+ },
226
+ json: {
227
+ type: 'boolean',
228
+ description: 'Output as JSON',
229
+ default: false,
230
+ },
231
+ },
232
+ async run({ args }) {
233
+ const result = await validate_plugin_via_cli(args.path);
234
+ if (args.json) {
235
+ output({
236
+ path: args.path,
237
+ valid: result.success,
238
+ output: result.stdout,
239
+ error: result.error,
240
+ }, true);
241
+ }
242
+ else if (result.success) {
243
+ console.log(result.stdout || 'Validation passed.');
244
+ }
245
+ else {
246
+ error(result.error || 'Validation failed');
247
+ }
248
+ },
249
+ });
195
250
  export default defineCommand({
196
251
  meta: {
197
252
  name: 'plugins',
198
253
  description: 'Manage Claude Code plugins',
199
254
  },
200
- subCommands: { list, enable, disable, install, uninstall, update },
255
+ subCommands: {
256
+ list,
257
+ enable,
258
+ disable,
259
+ install,
260
+ uninstall,
261
+ update,
262
+ validate,
263
+ },
201
264
  });
202
265
  //# sourceMappingURL=plugins.js.map
@@ -0,0 +1,36 @@
1
+ import { defineCommand } from 'citty';
2
+ import { output } from '../output.js';
3
+ export default defineCommand({
4
+ meta: {
5
+ name: 'reload',
6
+ description: 'Reload plugins in Claude Code',
7
+ },
8
+ args: {
9
+ json: {
10
+ type: 'boolean',
11
+ description: 'Output as JSON',
12
+ default: false,
13
+ },
14
+ },
15
+ async run({ args }) {
16
+ const info = {
17
+ supported: false,
18
+ message: 'Claude Code does not expose a programmatic reload API.',
19
+ instructions: [
20
+ 'Run /reload-plugins inside an active Claude Code session',
21
+ 'Or restart your Claude Code session to pick up changes',
22
+ ],
23
+ };
24
+ if (args.json) {
25
+ output(info, true);
26
+ }
27
+ else {
28
+ console.log(info.message);
29
+ console.log('\nTo reload plugins:');
30
+ for (const instruction of info.instructions) {
31
+ console.log(` - ${instruction}`);
32
+ }
33
+ }
34
+ },
35
+ });
36
+ //# sourceMappingURL=reload.js.map
@@ -0,0 +1,32 @@
1
+ import { defineCommand } from 'citty';
2
+ import { mcp_reset_project_choices_via_cli } from '../../utils/claude-cli.js';
3
+ import { error, output } from '../output.js';
4
+ export default defineCommand({
5
+ meta: {
6
+ name: 'reset-project-choices',
7
+ description: 'Reset all approved and rejected project-scoped MCP servers',
8
+ },
9
+ args: {
10
+ json: {
11
+ type: 'boolean',
12
+ description: 'Output as JSON',
13
+ default: false,
14
+ },
15
+ },
16
+ async run({ args }) {
17
+ const result = await mcp_reset_project_choices_via_cli();
18
+ if (args.json) {
19
+ output({
20
+ success: result.success,
21
+ error: result.error,
22
+ }, true);
23
+ }
24
+ else if (result.success) {
25
+ console.log('Project MCP server choices have been reset.');
26
+ }
27
+ else {
28
+ error(result.error || 'Unknown error');
29
+ }
30
+ },
31
+ });
32
+ //# sourceMappingURL=reset-project-choices.js.map
package/dist/cli/index.js CHANGED
@@ -10,12 +10,17 @@ const main = defineCommand({
10
10
  disable: () => import('./commands/disable.js').then((m) => m.default),
11
11
  remove: () => import('./commands/remove.js').then((m) => m.default),
12
12
  add: () => import('./commands/add.js').then((m) => m.default),
13
+ 'add-json': () => import('./commands/add-json.js').then((m) => m.default),
14
+ get: () => import('./commands/get.js').then((m) => m.default),
15
+ 'reset-project-choices': () => import('./commands/reset-project-choices.js').then((m) => m.default),
13
16
  backup: () => import('./commands/backup.js').then((m) => m.default),
14
17
  restore: () => import('./commands/restore.js').then((m) => m.default),
15
18
  profile: () => import('./commands/profile.js').then((m) => m.default),
16
19
  plugins: () => import('./commands/plugins.js').then((m) => m.default),
17
20
  cache: () => import('./commands/cache.js').then((m) => m.default),
18
21
  dev: () => import('./commands/dev.js').then((m) => m.default),
22
+ marketplace: () => import('./commands/marketplace.js').then((m) => m.default),
23
+ reload: () => import('./commands/reload.js').then((m) => m.default),
19
24
  },
20
25
  });
21
26
  export const run = () => runMain(main);
@@ -0,0 +1,275 @@
1
+ import { confirm, isCancel, log, multiselect, note, select, text, } from '@clack/prompts';
2
+ import { read_known_marketplaces, read_marketplace_manifest, } from '../core/plugin-cache.js';
3
+ import { get_all_plugins, read_claude_settings, } from '../core/settings.js';
4
+ 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';
5
+ /**
6
+ * Browse all available plugins across all marketplaces.
7
+ * Shows a multiselect with installed plugins pre-selected — toggle to install/uninstall.
8
+ */
9
+ async function handle_browse() {
10
+ const known = await read_known_marketplaces();
11
+ const marketplace_names = Object.keys(known);
12
+ if (marketplace_names.length === 0) {
13
+ note('No marketplaces configured.\nAdd one first to browse plugins.');
14
+ return;
15
+ }
16
+ // Build list of all available plugins across marketplaces
17
+ const all_available = [];
18
+ for (const mkt_name of marketplace_names) {
19
+ const manifest = await read_marketplace_manifest(mkt_name);
20
+ if (!manifest?.plugins?.length)
21
+ continue;
22
+ for (const p of manifest.plugins) {
23
+ all_available.push({
24
+ key: `${p.name}@${mkt_name}`,
25
+ name: p.name,
26
+ marketplace: mkt_name,
27
+ description: p.description,
28
+ });
29
+ }
30
+ }
31
+ if (all_available.length === 0) {
32
+ note('No plugins found in any marketplace.');
33
+ return;
34
+ }
35
+ // Get currently installed plugins
36
+ const settings = await read_claude_settings();
37
+ const installed = get_all_plugins(settings);
38
+ const installed_keys = new Set(installed.map((p) => `${p.name}@${p.marketplace}`));
39
+ const selected = await multiselect({
40
+ message: `Available plugins (${all_available.length}) — toggle to install/uninstall:`,
41
+ options: all_available.map((p) => ({
42
+ value: p.key,
43
+ label: p.name,
44
+ hint: `${p.marketplace}${p.description ? ` · ${p.description}` : ''}`,
45
+ })),
46
+ initialValues: all_available
47
+ .filter((p) => installed_keys.has(p.key))
48
+ .map((p) => p.key),
49
+ required: false,
50
+ });
51
+ if (isCancel(selected))
52
+ return;
53
+ const selected_set = new Set(selected);
54
+ // Determine what to install and uninstall
55
+ const to_install = all_available.filter((p) => selected_set.has(p.key) && !installed_keys.has(p.key));
56
+ const to_uninstall = all_available.filter((p) => !selected_set.has(p.key) && installed_keys.has(p.key));
57
+ if (to_install.length === 0 && to_uninstall.length === 0) {
58
+ log.info('No changes.');
59
+ return;
60
+ }
61
+ // Install new plugins
62
+ for (const p of to_install) {
63
+ log.info(`Installing ${p.key}...`);
64
+ const result = await install_plugin_via_cli(p.key);
65
+ if (result.success) {
66
+ log.success(`Installed: ${p.key}`);
67
+ }
68
+ else {
69
+ log.error(`Failed: ${p.key} - ${result.error}`);
70
+ }
71
+ }
72
+ // Uninstall deselected plugins
73
+ for (const p of to_uninstall) {
74
+ log.info(`Uninstalling ${p.key}...`);
75
+ const result = await uninstall_plugin_via_cli(p.key);
76
+ if (result.success) {
77
+ log.success(`Uninstalled: ${p.key}`);
78
+ }
79
+ else {
80
+ log.error(`Failed: ${p.key} - ${result.error}`);
81
+ }
82
+ }
83
+ const parts = [];
84
+ if (to_install.length > 0)
85
+ parts.push(`${to_install.length} installed`);
86
+ if (to_uninstall.length > 0)
87
+ parts.push(`${to_uninstall.length} uninstalled`);
88
+ note(parts.join(', '), 'Plugins updated');
89
+ }
90
+ async function handle_add() {
91
+ const source = await text({
92
+ message: 'Marketplace source:',
93
+ placeholder: 'e.g. owner/repo or https://github.com/owner/repo',
94
+ validate: (value) => {
95
+ if (!value || value.trim().length === 0) {
96
+ return 'Marketplace source is required';
97
+ }
98
+ },
99
+ });
100
+ if (isCancel(source))
101
+ return;
102
+ log.info(`Adding marketplace: ${source}`);
103
+ const result = await marketplace_add_via_cli(source);
104
+ if (!result.success) {
105
+ log.error(result.error || 'Unknown error');
106
+ return;
107
+ }
108
+ log.success(`Marketplace added: ${source}`);
109
+ // Try to find and offer available plugins
110
+ const marketplace_name = derive_marketplace_name(source);
111
+ const manifest = marketplace_name
112
+ ? await read_marketplace_manifest(marketplace_name)
113
+ : null;
114
+ if (!manifest?.plugins?.length) {
115
+ log.info('Install plugins with: mcpick plugins install <name>@<marketplace>');
116
+ return;
117
+ }
118
+ const should_install = await confirm({
119
+ message: `${manifest.plugins.length} plugins available. Install now?`,
120
+ });
121
+ if (isCancel(should_install) || !should_install)
122
+ return;
123
+ const to_install = await multiselect({
124
+ message: 'Select plugins to install:',
125
+ options: manifest.plugins.map((p) => ({
126
+ value: `${p.name}@${marketplace_name}`,
127
+ label: p.name,
128
+ hint: p.description,
129
+ })),
130
+ required: false,
131
+ });
132
+ if (isCancel(to_install) || to_install.length === 0)
133
+ return;
134
+ for (const key of to_install) {
135
+ log.info(`Installing ${key}...`);
136
+ const install_result = await install_plugin_via_cli(key);
137
+ if (install_result.success) {
138
+ log.success(`Installed: ${key}`);
139
+ }
140
+ else {
141
+ log.error(`Failed: ${key} - ${install_result.error}`);
142
+ }
143
+ }
144
+ }
145
+ function derive_marketplace_name(source) {
146
+ const match = source.match(/([^/]+?)(?:\.git)?$/);
147
+ return match ? match[1].toLowerCase() : null;
148
+ }
149
+ async function handle_remove() {
150
+ const known = await read_known_marketplaces();
151
+ const names = Object.keys(known);
152
+ if (names.length === 0) {
153
+ note('No marketplaces configured.');
154
+ return;
155
+ }
156
+ const name = await select({
157
+ message: 'Select marketplace to remove:',
158
+ options: names.map((n) => ({
159
+ value: n,
160
+ label: n,
161
+ hint: known[n].source.repo || known[n].source.url,
162
+ })),
163
+ });
164
+ if (isCancel(name))
165
+ return;
166
+ const should_remove = await confirm({
167
+ message: `Remove marketplace '${name}'? Its plugins will also be removed.`,
168
+ });
169
+ if (isCancel(should_remove) || !should_remove)
170
+ return;
171
+ const remove_result = await marketplace_remove_via_cli(name);
172
+ if (remove_result.success) {
173
+ log.success(`Marketplace removed: ${name}`);
174
+ }
175
+ else {
176
+ log.error(remove_result.error || 'Unknown error');
177
+ }
178
+ }
179
+ async function handle_update() {
180
+ const known = await read_known_marketplaces();
181
+ const names = Object.keys(known);
182
+ if (names.length === 0) {
183
+ note('No marketplaces configured.');
184
+ return;
185
+ }
186
+ const choice = await select({
187
+ message: 'What to update:',
188
+ options: [
189
+ { value: '__all__', label: 'All marketplaces' },
190
+ ...names.map((n) => ({
191
+ value: n,
192
+ label: n,
193
+ hint: known[n].source.repo || known[n].source.url,
194
+ })),
195
+ ],
196
+ });
197
+ if (isCancel(choice))
198
+ return;
199
+ if (choice === '__all__') {
200
+ log.info('Updating all marketplaces...');
201
+ const result = await marketplace_update_via_cli();
202
+ if (result.success) {
203
+ log.success('All marketplaces updated.');
204
+ }
205
+ else {
206
+ log.error(result.error || 'Unknown error');
207
+ }
208
+ }
209
+ else {
210
+ log.info(`Updating ${choice}...`);
211
+ const result = await marketplace_update_via_cli(choice);
212
+ if (result.success) {
213
+ log.success(`Marketplace updated: ${choice}`);
214
+ }
215
+ else {
216
+ log.error(result.error || 'Unknown error');
217
+ }
218
+ }
219
+ }
220
+ export async function manage_marketplace() {
221
+ while (true) {
222
+ const action = await select({
223
+ message: 'Marketplace & plugins:',
224
+ options: [
225
+ {
226
+ value: 'browse',
227
+ label: 'Browse & install plugins',
228
+ hint: 'Toggle plugins on/off across all marketplaces',
229
+ },
230
+ {
231
+ value: 'add',
232
+ label: 'Add marketplace',
233
+ hint: 'Add a plugin catalog, then install plugins from it',
234
+ },
235
+ {
236
+ value: 'remove',
237
+ label: 'Remove marketplace',
238
+ hint: 'Remove a marketplace and its plugins',
239
+ },
240
+ {
241
+ value: 'update',
242
+ label: 'Update marketplace(s)',
243
+ hint: 'Pull latest from source',
244
+ },
245
+ {
246
+ value: 'back',
247
+ label: 'Back',
248
+ hint: 'Return to main menu',
249
+ },
250
+ ],
251
+ });
252
+ if (isCancel(action) || action === 'back')
253
+ return;
254
+ try {
255
+ switch (action) {
256
+ case 'browse':
257
+ await handle_browse();
258
+ break;
259
+ case 'add':
260
+ await handle_add();
261
+ break;
262
+ case 'remove':
263
+ await handle_remove();
264
+ break;
265
+ case 'update':
266
+ await handle_update();
267
+ break;
268
+ }
269
+ }
270
+ catch (err) {
271
+ log.error(err instanceof Error ? err.message : 'Unknown error');
272
+ }
273
+ }
274
+ }
275
+ //# sourceMappingURL=manage-marketplace.js.map
@@ -82,8 +82,7 @@ async function write_server_to_scope(name, server, scope) {
82
82
  if (!existing.mcpServers) {
83
83
  existing.mcpServers = {};
84
84
  }
85
- existing.mcpServers[name] =
86
- server;
85
+ existing.mcpServers[name] = server;
87
86
  return existing;
88
87
  });
89
88
  }
@@ -100,7 +99,8 @@ async function write_server_to_scope(name, server, scope) {
100
99
  if (!projects[cwd].mcpServers) {
101
100
  projects[cwd].mcpServers = {};
102
101
  }
103
- projects[cwd].mcpServers[name] = server;
102
+ projects[cwd].mcpServers[name] =
103
+ server;
104
104
  return existing;
105
105
  });
106
106
  }
@@ -1,5 +1,5 @@
1
1
  import { exec } from 'node:child_process';
2
- import { lstat, readdir, readFile, readlink, rename, rm, symlink } 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';
@@ -50,6 +50,51 @@ async function get_marketplace_head_sha(marketplace_path) {
50
50
  return null;
51
51
  }
52
52
  }
53
+ /**
54
+ * Recover a marketplace clone stuck on a deleted branch.
55
+ * Resets the fetch refspec, fetches all branches, and checks out the default branch.
56
+ */
57
+ async function recover_deleted_branch(dir) {
58
+ const q = JSON.stringify(dir);
59
+ try {
60
+ // Reset narrow refspec to fetch all branches
61
+ await execAsync(`git -C ${q} remote set-branches origin '*'`, {
62
+ timeout: 10_000,
63
+ });
64
+ await execAsync(`git -C ${q} fetch origin`, {
65
+ timeout: 30_000,
66
+ });
67
+ // Detect default branch
68
+ let default_branch = 'main';
69
+ try {
70
+ const { stdout } = await execAsync(`git -C ${q} symbolic-ref refs/remotes/origin/HEAD`, { timeout: 5_000 });
71
+ const match = stdout
72
+ .trim()
73
+ .match(/refs\/remotes\/origin\/(.+)/);
74
+ if (match)
75
+ default_branch = match[1];
76
+ }
77
+ catch {
78
+ // symbolic-ref not set — try main, then master
79
+ try {
80
+ await execAsync(`git -C ${q} rev-parse --verify origin/main`, { timeout: 5_000 });
81
+ default_branch = 'main';
82
+ }
83
+ catch {
84
+ default_branch = 'master';
85
+ }
86
+ }
87
+ await execAsync(`git -C ${q} checkout ${default_branch}`, {
88
+ timeout: 10_000,
89
+ });
90
+ await execAsync(`git -C ${q} reset --hard origin/${default_branch}`, { timeout: 10_000 });
91
+ return { recovered: true };
92
+ }
93
+ catch (err) {
94
+ const msg = err instanceof Error ? err.message : 'Unknown error';
95
+ return { recovered: false, error: msg };
96
+ }
97
+ }
53
98
  export async function refresh_marketplace(name, marketplace) {
54
99
  const dir = marketplace.installLocation;
55
100
  try {
@@ -58,9 +103,16 @@ export async function refresh_marketplace(name, marketplace) {
58
103
  });
59
104
  return { success: true };
60
105
  }
61
- catch (err) {
62
- const message = err instanceof Error ? err.message : 'Unknown error';
63
- return { success: false, error: `${name}: ${message}` };
106
+ catch {
107
+ // Fast-forward failed attempt recovery from deleted branch
108
+ const recovery = await recover_deleted_branch(dir);
109
+ if (recovery.recovered) {
110
+ return { success: true };
111
+ }
112
+ return {
113
+ success: false,
114
+ error: `${name}: recovery failed: ${recovery.error}`,
115
+ };
64
116
  }
65
117
  }
66
118
  export async function refresh_all_marketplaces() {
@@ -155,6 +207,42 @@ export async function get_cached_plugins_info() {
155
207
  }
156
208
  return results;
157
209
  }
210
+ // --- Cache scanning ---
211
+ /**
212
+ * Scan the cache directory on disk to find all plugin keys,
213
+ * including marketplace-sourced plugins not tracked in installed_plugins.json.
214
+ */
215
+ export async function scan_all_cache_keys() {
216
+ const cache_dir = get_plugin_cache_dir();
217
+ const keys = [];
218
+ try {
219
+ const marketplaces = await readdir(cache_dir, {
220
+ withFileTypes: true,
221
+ });
222
+ for (const mkt of marketplaces) {
223
+ if (!mkt.isDirectory() && !mkt.isSymbolicLink())
224
+ continue;
225
+ const mkt_path = join(cache_dir, mkt.name);
226
+ try {
227
+ const plugins = await readdir(mkt_path, {
228
+ withFileTypes: true,
229
+ });
230
+ for (const plugin of plugins) {
231
+ if (!plugin.isDirectory() && !plugin.isSymbolicLink())
232
+ continue;
233
+ keys.push(`${plugin.name}@${mkt.name}`);
234
+ }
235
+ }
236
+ catch {
237
+ // Skip unreadable marketplace dirs
238
+ }
239
+ }
240
+ }
241
+ catch {
242
+ // Cache dir doesn't exist
243
+ }
244
+ return keys;
245
+ }
158
246
  // --- Cache clearing ---
159
247
  function is_safe_cache_path(path) {
160
248
  const cache_dir = resolve(get_plugin_cache_dir());
@@ -377,7 +465,9 @@ export async function list_linked_plugins() {
377
465
  const cache_dir = get_plugin_cache_dir();
378
466
  const links = [];
379
467
  try {
380
- const marketplaces = await readdir(cache_dir, { withFileTypes: true });
468
+ const marketplaces = await readdir(cache_dir, {
469
+ withFileTypes: true,
470
+ });
381
471
  for (const mkt of marketplaces) {
382
472
  if (!mkt.isDirectory() && !mkt.isSymbolicLink())
383
473
  continue;
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ import { backup_config } from './commands/backup.js';
5
5
  import { edit_config } from './commands/edit-config.js';
6
6
  import { edit_plugins } from './commands/edit-plugins.js';
7
7
  import { manage_cache } from './commands/manage-cache.js';
8
+ import { manage_marketplace } from './commands/manage-marketplace.js';
8
9
  import { restore_config } from './commands/restore.js';
9
10
  import { write_claude_config } from './core/config.js';
10
11
  import { list_profiles, load_profile, save_profile, } from './core/profile.js';
@@ -178,6 +179,11 @@ async function main() {
178
179
  label: 'Manage plugins',
179
180
  hint: 'Toggle, install, uninstall, or update plugins',
180
181
  },
182
+ {
183
+ value: 'manage-marketplace',
184
+ label: 'Manage marketplaces',
185
+ hint: 'Add, remove, or update plugin marketplaces',
186
+ },
181
187
  {
182
188
  value: 'manage-cache',
183
189
  label: 'Manage plugin cache',
@@ -226,6 +232,9 @@ async function main() {
226
232
  case 'edit-plugins':
227
233
  await edit_plugins();
228
234
  break;
235
+ case 'manage-marketplace':
236
+ await manage_marketplace();
237
+ break;
229
238
  case 'manage-cache':
230
239
  await manage_cache();
231
240
  break;
@@ -276,12 +285,17 @@ const SUBCOMMANDS = new Set([
276
285
  'disable',
277
286
  'remove',
278
287
  'add',
288
+ 'add-json',
289
+ 'get',
290
+ 'reset-project-choices',
279
291
  'backup',
280
292
  'restore',
281
293
  'profile',
282
294
  'plugins',
283
295
  'cache',
284
296
  'dev',
297
+ 'marketplace',
298
+ 'reload',
285
299
  ]);
286
300
  const arg = process.argv[2];
287
301
  if ((arg && SUBCOMMANDS.has(arg)) ||
@@ -189,6 +189,170 @@ export async function update_plugin_via_cli(key, scope = 'user') {
189
189
  };
190
190
  }
191
191
  }
192
+ /**
193
+ * Add a marketplace via Claude CLI
194
+ */
195
+ /**
196
+ * Extract GitHub owner/repo from various source formats.
197
+ * Returns null if not a recognizable GitHub reference.
198
+ */
199
+ function parse_github_repo(source) {
200
+ // HTTPS URL: https://github.com/owner/repo[.git]
201
+ const https_match = source.match(/^https?:\/\/github\.com\/([^/]+)\/([^/.]+)(?:\.git)?$/);
202
+ if (https_match)
203
+ return { owner: https_match[1], repo: https_match[2] };
204
+ // SSH URL: git@github.com:owner/repo[.git]
205
+ const ssh_match = source.match(/^git@github\.com:([^/]+)\/([^/.]+)(?:\.git)?$/);
206
+ if (ssh_match)
207
+ return { owner: ssh_match[1], repo: ssh_match[2] };
208
+ // Shorthand: owner/repo (no slashes beyond the one separator)
209
+ const shorthand_match = source.match(/^([^/\s]+)\/([^/\s]+)$/);
210
+ if (shorthand_match)
211
+ return { owner: shorthand_match[1], repo: shorthand_match[2] };
212
+ return null;
213
+ }
214
+ /**
215
+ * Validate that a GitHub repository exists and is accessible.
216
+ * Returns an error message if validation fails, null if OK.
217
+ */
218
+ async function validate_github_repo(owner, repo) {
219
+ try {
220
+ const response = await fetch(`https://api.github.com/repos/${owner}/${repo}`, {
221
+ method: 'GET',
222
+ headers: { Accept: 'application/vnd.github.v3+json' },
223
+ });
224
+ if (response.status === 200)
225
+ return null;
226
+ if (response.status === 404) {
227
+ return `Repository '${owner}/${repo}' not found on GitHub. Check the name or ensure it's not private.`;
228
+ }
229
+ if (response.status === 403) {
230
+ return `Access denied for '${owner}/${repo}'. The repository may be private — configure a GitHub token or use SSH.`;
231
+ }
232
+ return `GitHub API returned status ${response.status} for '${owner}/${repo}'.`;
233
+ }
234
+ catch {
235
+ // Network error — skip validation and let the CLI attempt the clone
236
+ return null;
237
+ }
238
+ }
239
+ export async function marketplace_add_via_cli(source) {
240
+ const cli_available = await check_claude_cli();
241
+ if (!cli_available) {
242
+ return {
243
+ success: false,
244
+ error: 'Claude CLI not found. Please install Claude Code CLI.',
245
+ };
246
+ }
247
+ // Validate GitHub repo exists before attempting clone
248
+ // Only validate shorthand (owner/repo) — explicit URLs imply the user knows the repo
249
+ const gh = parse_github_repo(source);
250
+ const is_shorthand = gh && !source.startsWith('http') && !source.startsWith('git@');
251
+ if (gh && is_shorthand) {
252
+ const validation_error = await validate_github_repo(gh.owner, gh.repo);
253
+ if (validation_error) {
254
+ return { success: false, error: validation_error };
255
+ }
256
+ }
257
+ try {
258
+ await execAsync(`claude plugin marketplace add ${shell_escape(source)}`);
259
+ return { success: true };
260
+ }
261
+ catch (error) {
262
+ const message = error instanceof Error ? error.message : 'Unknown error';
263
+ // Provide clearer error messages for common failures
264
+ if (message.includes('SSH') ||
265
+ message.includes('Permission denied (publickey)')) {
266
+ return {
267
+ success: false,
268
+ error: `SSH authentication failed for '${source}'. Either:\n - Configure SSH keys: https://docs.github.com/en/authentication/connecting-to-github-with-ssh\n - Use HTTPS URL instead: https://github.com/${gh ? `${gh.owner}/${gh.repo}` : source}`,
269
+ };
270
+ }
271
+ if (message.includes('not found') ||
272
+ message.includes('does not exist')) {
273
+ return {
274
+ success: false,
275
+ error: `Repository '${source}' not found. Check the name and your access permissions.`,
276
+ };
277
+ }
278
+ return {
279
+ success: false,
280
+ error: `Failed to add marketplace: ${message}`,
281
+ };
282
+ }
283
+ }
284
+ /**
285
+ * Remove a marketplace via Claude CLI
286
+ */
287
+ export async function marketplace_remove_via_cli(name) {
288
+ const cli_available = await check_claude_cli();
289
+ if (!cli_available) {
290
+ return {
291
+ success: false,
292
+ error: 'Claude CLI not found. Please install Claude Code CLI.',
293
+ };
294
+ }
295
+ try {
296
+ await execAsync(`claude plugin marketplace remove ${shell_escape(name)}`);
297
+ return { success: true };
298
+ }
299
+ catch (error) {
300
+ const message = error instanceof Error ? error.message : 'Unknown error';
301
+ return {
302
+ success: false,
303
+ error: `Failed to remove marketplace: ${message}`,
304
+ };
305
+ }
306
+ }
307
+ /**
308
+ * Update marketplace(s) via Claude CLI
309
+ */
310
+ export async function marketplace_update_via_cli(name) {
311
+ const cli_available = await check_claude_cli();
312
+ if (!cli_available) {
313
+ return {
314
+ success: false,
315
+ error: 'Claude CLI not found. Please install Claude Code CLI.',
316
+ };
317
+ }
318
+ try {
319
+ const cmd = name
320
+ ? `claude plugin marketplace update ${shell_escape(name)}`
321
+ : 'claude plugin marketplace update';
322
+ await execAsync(cmd);
323
+ return { success: true };
324
+ }
325
+ catch (error) {
326
+ const message = error instanceof Error ? error.message : 'Unknown error';
327
+ return {
328
+ success: false,
329
+ error: `Failed to update marketplace: ${message}`,
330
+ };
331
+ }
332
+ }
333
+ /**
334
+ * List marketplaces via Claude CLI
335
+ */
336
+ export async function marketplace_list_via_cli() {
337
+ const cli_available = await check_claude_cli();
338
+ if (!cli_available) {
339
+ return {
340
+ success: false,
341
+ error: 'Claude CLI not found. Please install Claude Code CLI.',
342
+ };
343
+ }
344
+ try {
345
+ const { stdout } = await execAsync('claude plugin marketplace list');
346
+ return { success: true, stdout: stdout.trim() };
347
+ }
348
+ catch (error) {
349
+ const message = error instanceof Error ? error.message : 'Unknown error';
350
+ return {
351
+ success: false,
352
+ error: `Failed to list marketplaces: ${message}`,
353
+ };
354
+ }
355
+ }
192
356
  /**
193
357
  * Get the scope description for display
194
358
  */
@@ -202,6 +366,98 @@ export function get_scope_description(scope) {
202
366
  return 'Global - all projects';
203
367
  }
204
368
  }
369
+ /**
370
+ * Validate a plugin or marketplace manifest via Claude CLI
371
+ */
372
+ export async function validate_plugin_via_cli(path) {
373
+ const cli_available = await check_claude_cli();
374
+ if (!cli_available) {
375
+ return {
376
+ success: false,
377
+ error: 'Claude CLI not found. Please install Claude Code CLI.',
378
+ };
379
+ }
380
+ try {
381
+ const { stdout } = await execAsync(`claude plugin validate ${shell_escape(path)}`);
382
+ return { success: true, stdout: stdout.trim() };
383
+ }
384
+ catch (error) {
385
+ const message = error instanceof Error ? error.message : 'Unknown error';
386
+ return {
387
+ success: false,
388
+ error: `Validation failed: ${message}`,
389
+ };
390
+ }
391
+ }
392
+ /**
393
+ * Get details about an MCP server via Claude CLI
394
+ */
395
+ export async function mcp_get_via_cli(name) {
396
+ const cli_available = await check_claude_cli();
397
+ if (!cli_available) {
398
+ return {
399
+ success: false,
400
+ error: 'Claude CLI not found. Please install Claude Code CLI.',
401
+ };
402
+ }
403
+ try {
404
+ const { stdout } = await execAsync(`claude mcp get ${shell_escape(name)}`);
405
+ return { success: true, stdout: stdout.trim() };
406
+ }
407
+ catch (error) {
408
+ const message = error instanceof Error ? error.message : 'Unknown error';
409
+ return {
410
+ success: false,
411
+ error: `Failed to get server details: ${message}`,
412
+ };
413
+ }
414
+ }
415
+ /**
416
+ * Add an MCP server from raw JSON via Claude CLI
417
+ */
418
+ export async function mcp_add_json_via_cli(name, json, scope = 'local') {
419
+ const cli_available = await check_claude_cli();
420
+ if (!cli_available) {
421
+ return {
422
+ success: false,
423
+ error: 'Claude CLI not found. Please install Claude Code CLI.',
424
+ };
425
+ }
426
+ try {
427
+ await execAsync(`claude mcp add-json ${shell_escape(name)} ${shell_escape(json)} --scope ${scope}`);
428
+ return { success: true };
429
+ }
430
+ catch (error) {
431
+ const message = error instanceof Error ? error.message : 'Unknown error';
432
+ return {
433
+ success: false,
434
+ error: `Failed to add server from JSON: ${message}`,
435
+ };
436
+ }
437
+ }
438
+ /**
439
+ * Reset project-scoped MCP server choices via Claude CLI
440
+ */
441
+ export async function mcp_reset_project_choices_via_cli() {
442
+ const cli_available = await check_claude_cli();
443
+ if (!cli_available) {
444
+ return {
445
+ success: false,
446
+ error: 'Claude CLI not found. Please install Claude Code CLI.',
447
+ };
448
+ }
449
+ try {
450
+ await execAsync('claude mcp reset-project-choices');
451
+ return { success: true };
452
+ }
453
+ catch (error) {
454
+ const message = error instanceof Error ? error.message : 'Unknown error';
455
+ return {
456
+ success: false,
457
+ error: `Failed to reset project choices: ${message}`,
458
+ };
459
+ }
460
+ }
205
461
  /**
206
462
  * Get scope options for select prompt
207
463
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcpick",
3
- "version": "0.0.14",
3
+ "version": "0.0.16",
4
4
  "description": "Dynamic MCP server and plugin configuration manager for Claude Code",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",