mcpick 0.0.10 → 0.0.12
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 +17 -0
- package/README.md +114 -4
- package/dist/cli/commands/backup.js +50 -22
- package/dist/cli/commands/plugins.js +110 -1
- package/dist/cli/commands/profile.js +31 -9
- package/dist/cli/commands/remove.js +12 -8
- package/dist/cli/commands/restore.js +75 -26
- package/dist/commands/backup.js +28 -22
- package/dist/commands/edit-plugins.js +192 -47
- package/dist/commands/restore.js +85 -34
- package/dist/core/config.js +6 -15
- package/dist/core/plugin-cache.js +3 -2
- package/dist/core/profile.js +32 -8
- package/dist/core/registry.js +44 -30
- package/dist/core/settings.js +8 -15
- package/dist/index.js +48 -19
- package/dist/utils/atomic-write.js +27 -0
- package/dist/utils/claude-cli.js +69 -0
- package/dist/utils/paths.js +8 -2
- package/package.json +1 -1
package/dist/core/registry.js
CHANGED
|
@@ -40,7 +40,17 @@ export async function add_server_to_registry(server) {
|
|
|
40
40
|
await write_server_registry(registry);
|
|
41
41
|
}
|
|
42
42
|
export async function get_all_available_servers() {
|
|
43
|
+
const { get_enabled_servers, read_claude_config } = await import('./config.js');
|
|
43
44
|
const registry = await read_server_registry();
|
|
45
|
+
const config = await read_claude_config();
|
|
46
|
+
const config_servers = get_enabled_servers(config);
|
|
47
|
+
// Merge: registry is base, config servers fill in any missing
|
|
48
|
+
const known_names = new Set(registry.servers.map((s) => s.name));
|
|
49
|
+
for (const server of config_servers) {
|
|
50
|
+
if (!known_names.has(server.name)) {
|
|
51
|
+
registry.servers.push(server);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
44
54
|
return registry.servers;
|
|
45
55
|
}
|
|
46
56
|
export async function sync_servers_to_registry(servers) {
|
|
@@ -56,36 +66,40 @@ export async function sync_servers_to_registry(servers) {
|
|
|
56
66
|
});
|
|
57
67
|
await write_server_registry(registry);
|
|
58
68
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
catch (error) {
|
|
83
|
-
if (error instanceof Error &&
|
|
84
|
-
'code' in error &&
|
|
85
|
-
error.code === 'ENOENT') {
|
|
86
|
-
return [];
|
|
69
|
+
function parse_backups(prefix, pattern) {
|
|
70
|
+
return async () => {
|
|
71
|
+
const backups_dir = get_backups_dir();
|
|
72
|
+
try {
|
|
73
|
+
await access(backups_dir);
|
|
74
|
+
const files = await readdir(backups_dir);
|
|
75
|
+
const backup_files = files
|
|
76
|
+
.filter((file) => file.startsWith(prefix) && file.endsWith('.json'))
|
|
77
|
+
.map((file) => {
|
|
78
|
+
const timestamp_match = file.match(pattern);
|
|
79
|
+
if (!timestamp_match)
|
|
80
|
+
return null;
|
|
81
|
+
const [, year, month, day, hour, minute, second] = timestamp_match;
|
|
82
|
+
const timestamp = new Date(parseInt(year), parseInt(month) - 1, parseInt(day), parseInt(hour), parseInt(minute), parseInt(second));
|
|
83
|
+
return {
|
|
84
|
+
filename: file,
|
|
85
|
+
timestamp,
|
|
86
|
+
path: join(backups_dir, file),
|
|
87
|
+
};
|
|
88
|
+
})
|
|
89
|
+
.filter((backup) => backup !== null)
|
|
90
|
+
.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
|
|
91
|
+
return backup_files;
|
|
87
92
|
}
|
|
88
|
-
|
|
89
|
-
|
|
93
|
+
catch (error) {
|
|
94
|
+
if (error instanceof Error &&
|
|
95
|
+
'code' in error &&
|
|
96
|
+
error.code === 'ENOENT') {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
90
102
|
}
|
|
103
|
+
export const list_backups = parse_backups('mcp-servers-', /mcp-servers-(\d{4})-(\d{2})-(\d{2})-(\d{2})(\d{2})(\d{2})\.json/);
|
|
104
|
+
export const list_plugin_backups = parse_backups('plugins-', /plugins-(\d{4})-(\d{2})-(\d{2})-(\d{2})(\d{2})(\d{2})\.json/);
|
|
91
105
|
//# sourceMappingURL=registry.js.map
|
package/dist/core/settings.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { access, readFile
|
|
1
|
+
import { access, readFile } from 'node:fs/promises';
|
|
2
|
+
import { atomic_json_write } from '../utils/atomic-write.js';
|
|
2
3
|
import { get_claude_settings_path } from '../utils/paths.js';
|
|
3
4
|
export async function read_claude_settings() {
|
|
4
5
|
const settings_path = get_claude_settings_path();
|
|
@@ -17,20 +18,12 @@ export async function read_claude_settings() {
|
|
|
17
18
|
}
|
|
18
19
|
}
|
|
19
20
|
export async function write_claude_settings(updates) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
existing
|
|
25
|
-
}
|
|
26
|
-
catch {
|
|
27
|
-
// Start with empty if file doesn't exist
|
|
28
|
-
}
|
|
29
|
-
// Merge only the keys we're updating
|
|
30
|
-
for (const [key, value] of Object.entries(updates)) {
|
|
31
|
-
existing[key] = value;
|
|
32
|
-
}
|
|
33
|
-
await writeFile(settings_path, JSON.stringify(existing, null, 2), 'utf-8');
|
|
21
|
+
await atomic_json_write(get_claude_settings_path(), (existing) => {
|
|
22
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
23
|
+
existing[key] = value;
|
|
24
|
+
}
|
|
25
|
+
return existing;
|
|
26
|
+
});
|
|
34
27
|
}
|
|
35
28
|
/**
|
|
36
29
|
* Parse enabledPlugins into structured list.
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import { manage_cache } from './commands/manage-cache.js';
|
|
|
8
8
|
import { restore_config } from './commands/restore.js';
|
|
9
9
|
import { write_claude_config } from './core/config.js';
|
|
10
10
|
import { list_profiles, load_profile, save_profile, } from './core/profile.js';
|
|
11
|
+
import { write_claude_settings } from './core/settings.js';
|
|
11
12
|
function parse_args() {
|
|
12
13
|
const args = process.argv.slice(2);
|
|
13
14
|
const result = {};
|
|
@@ -29,10 +30,17 @@ function parse_args() {
|
|
|
29
30
|
async function apply_profile(name) {
|
|
30
31
|
intro(`MCPick - Loading profile: ${name}`);
|
|
31
32
|
try {
|
|
32
|
-
const
|
|
33
|
-
await write_claude_config(
|
|
34
|
-
const server_count = Object.keys(
|
|
35
|
-
|
|
33
|
+
const profile = await load_profile(name);
|
|
34
|
+
await write_claude_config(profile.config);
|
|
35
|
+
const server_count = Object.keys(profile.config.mcpServers || {}).length;
|
|
36
|
+
const parts = [`${server_count} servers`];
|
|
37
|
+
if (profile.enabledPlugins) {
|
|
38
|
+
await write_claude_settings({
|
|
39
|
+
enabledPlugins: profile.enabledPlugins,
|
|
40
|
+
});
|
|
41
|
+
parts.push(`${Object.keys(profile.enabledPlugins).length} plugins`);
|
|
42
|
+
}
|
|
43
|
+
log.success(`Profile '${name}' applied (${parts.join(', ')})`);
|
|
36
44
|
outro('Done!');
|
|
37
45
|
}
|
|
38
46
|
catch (error) {
|
|
@@ -62,8 +70,12 @@ async function show_profiles() {
|
|
|
62
70
|
async function create_profile(name) {
|
|
63
71
|
intro(`MCPick - Saving profile: ${name}`);
|
|
64
72
|
try {
|
|
65
|
-
const
|
|
66
|
-
|
|
73
|
+
const counts = await save_profile(name);
|
|
74
|
+
const parts = [`${counts.serverCount} servers`];
|
|
75
|
+
if (counts.pluginCount > 0) {
|
|
76
|
+
parts.push(`${counts.pluginCount} plugins`);
|
|
77
|
+
}
|
|
78
|
+
log.success(`Profile '${name}' saved (${parts.join(', ')})`);
|
|
67
79
|
outro('Done!');
|
|
68
80
|
}
|
|
69
81
|
catch (error) {
|
|
@@ -85,18 +97,31 @@ async function handle_load_profile() {
|
|
|
85
97
|
}
|
|
86
98
|
const profile_name = await select({
|
|
87
99
|
message: 'Select a profile to load:',
|
|
88
|
-
options: profiles.map((p) =>
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
100
|
+
options: profiles.map((p) => {
|
|
101
|
+
const parts = [`${p.serverCount} servers`];
|
|
102
|
+
if (p.pluginCount > 0) {
|
|
103
|
+
parts.push(`${p.pluginCount} plugins`);
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
value: p.name,
|
|
107
|
+
label: p.name,
|
|
108
|
+
hint: parts.join(', '),
|
|
109
|
+
};
|
|
110
|
+
}),
|
|
93
111
|
});
|
|
94
112
|
if (isCancel(profile_name))
|
|
95
113
|
return;
|
|
96
|
-
const
|
|
97
|
-
await write_claude_config(
|
|
98
|
-
const server_count = Object.keys(
|
|
99
|
-
|
|
114
|
+
const profile = await load_profile(profile_name);
|
|
115
|
+
await write_claude_config(profile.config);
|
|
116
|
+
const server_count = Object.keys(profile.config.mcpServers || {}).length;
|
|
117
|
+
const parts = [`${server_count} servers`];
|
|
118
|
+
if (profile.enabledPlugins) {
|
|
119
|
+
await write_claude_settings({
|
|
120
|
+
enabledPlugins: profile.enabledPlugins,
|
|
121
|
+
});
|
|
122
|
+
parts.push(`${Object.keys(profile.enabledPlugins).length} plugins`);
|
|
123
|
+
}
|
|
124
|
+
log.success(`Profile '${profile_name}' applied (${parts.join(', ')})`);
|
|
100
125
|
}
|
|
101
126
|
async function handle_save_profile() {
|
|
102
127
|
const name = await text({
|
|
@@ -113,8 +138,12 @@ async function handle_save_profile() {
|
|
|
113
138
|
});
|
|
114
139
|
if (isCancel(name))
|
|
115
140
|
return;
|
|
116
|
-
const
|
|
117
|
-
|
|
141
|
+
const counts = await save_profile(name);
|
|
142
|
+
const parts = [`${counts.serverCount} servers`];
|
|
143
|
+
if (counts.pluginCount > 0) {
|
|
144
|
+
parts.push(`${counts.pluginCount} plugins`);
|
|
145
|
+
}
|
|
146
|
+
log.success(`Profile '${name}' saved (${parts.join(', ')})`);
|
|
118
147
|
}
|
|
119
148
|
async function main() {
|
|
120
149
|
const args = parse_args();
|
|
@@ -146,8 +175,8 @@ async function main() {
|
|
|
146
175
|
},
|
|
147
176
|
{
|
|
148
177
|
value: 'edit-plugins',
|
|
149
|
-
label: '
|
|
150
|
-
hint: 'Toggle
|
|
178
|
+
label: 'Manage plugins',
|
|
179
|
+
hint: 'Toggle, install, uninstall, or update plugins',
|
|
151
180
|
},
|
|
152
181
|
{
|
|
153
182
|
value: 'manage-cache',
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { readFile, rename, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Atomically write a JSON file with fresh-read merging.
|
|
5
|
+
*
|
|
6
|
+
* 1. Re-reads the file right before writing to pick up concurrent changes
|
|
7
|
+
* 2. Applies the merge function to the freshest data
|
|
8
|
+
* 3. Writes to a temp file, then renames (atomic on same filesystem)
|
|
9
|
+
*/
|
|
10
|
+
export async function atomic_json_write(file_path, merge) {
|
|
11
|
+
// Read the freshest version right before writing
|
|
12
|
+
let existing = {};
|
|
13
|
+
try {
|
|
14
|
+
const content = await readFile(file_path, 'utf-8');
|
|
15
|
+
existing = JSON.parse(content);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// File doesn't exist or invalid — start fresh
|
|
19
|
+
}
|
|
20
|
+
const merged = merge(existing);
|
|
21
|
+
const content = JSON.stringify(merged, null, 2);
|
|
22
|
+
// Write to temp file then rename for atomicity
|
|
23
|
+
const tmp_path = join(dirname(file_path), `.${Date.now()}.tmp`);
|
|
24
|
+
await writeFile(tmp_path, content, 'utf-8');
|
|
25
|
+
await rename(tmp_path, file_path);
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=atomic-write.js.map
|
package/dist/utils/claude-cli.js
CHANGED
|
@@ -115,6 +115,75 @@ export async function remove_mcp_via_cli(name) {
|
|
|
115
115
|
};
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Install a plugin via Claude CLI
|
|
120
|
+
*/
|
|
121
|
+
export async function install_plugin_via_cli(key, scope = 'user') {
|
|
122
|
+
const cli_available = await check_claude_cli();
|
|
123
|
+
if (!cli_available) {
|
|
124
|
+
return {
|
|
125
|
+
success: false,
|
|
126
|
+
error: 'Claude CLI not found. Please install Claude Code CLI.',
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
await execAsync(`claude plugin install ${shell_escape(key)} --scope ${scope}`);
|
|
131
|
+
return { success: true };
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
135
|
+
return {
|
|
136
|
+
success: false,
|
|
137
|
+
error: `Failed to install plugin: ${message}`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Uninstall a plugin via Claude CLI
|
|
143
|
+
*/
|
|
144
|
+
export async function uninstall_plugin_via_cli(key, scope = 'user') {
|
|
145
|
+
const cli_available = await check_claude_cli();
|
|
146
|
+
if (!cli_available) {
|
|
147
|
+
return {
|
|
148
|
+
success: false,
|
|
149
|
+
error: 'Claude CLI not found. Please install Claude Code CLI.',
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
await execAsync(`claude plugin uninstall ${shell_escape(key)} --scope ${scope}`);
|
|
154
|
+
return { success: true };
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
158
|
+
return {
|
|
159
|
+
success: false,
|
|
160
|
+
error: `Failed to uninstall plugin: ${message}`,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Update a plugin via Claude CLI
|
|
166
|
+
*/
|
|
167
|
+
export async function update_plugin_via_cli(key, scope = 'user') {
|
|
168
|
+
const cli_available = await check_claude_cli();
|
|
169
|
+
if (!cli_available) {
|
|
170
|
+
return {
|
|
171
|
+
success: false,
|
|
172
|
+
error: 'Claude CLI not found. Please install Claude Code CLI.',
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
await execAsync(`claude plugin update ${shell_escape(key)} --scope ${scope}`);
|
|
177
|
+
return { success: true };
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
181
|
+
return {
|
|
182
|
+
success: false,
|
|
183
|
+
error: `Failed to update plugin: ${message}`,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
118
187
|
/**
|
|
119
188
|
* Get the scope description for display
|
|
120
189
|
*/
|
package/dist/utils/paths.js
CHANGED
|
@@ -45,7 +45,7 @@ export function get_profile_path(name) {
|
|
|
45
45
|
const filename = name.endsWith('.json') ? name : `${name}.json`;
|
|
46
46
|
return join(get_profiles_dir(), filename);
|
|
47
47
|
}
|
|
48
|
-
|
|
48
|
+
function format_backup_timestamp() {
|
|
49
49
|
const now = new Date();
|
|
50
50
|
const year = now.getFullYear();
|
|
51
51
|
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
@@ -53,7 +53,13 @@ export function get_backup_filename() {
|
|
|
53
53
|
const hour = String(now.getHours()).padStart(2, '0');
|
|
54
54
|
const minute = String(now.getMinutes()).padStart(2, '0');
|
|
55
55
|
const second = String(now.getSeconds()).padStart(2, '0');
|
|
56
|
-
return
|
|
56
|
+
return `${year}-${month}-${day}-${hour}${minute}${second}`;
|
|
57
|
+
}
|
|
58
|
+
export function get_backup_filename() {
|
|
59
|
+
return `mcp-servers-${format_backup_timestamp()}.json`;
|
|
60
|
+
}
|
|
61
|
+
export function get_plugin_backup_filename() {
|
|
62
|
+
return `plugins-${format_backup_timestamp()}.json`;
|
|
57
63
|
}
|
|
58
64
|
export async function ensure_directory_exists(dir_path) {
|
|
59
65
|
try {
|