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.
@@ -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
- export async function list_backups() {
60
- const backups_dir = get_backups_dir();
61
- try {
62
- await access(backups_dir);
63
- const files = await readdir(backups_dir);
64
- const backup_files = files
65
- .filter((file) => file.startsWith('mcp-servers-') && file.endsWith('.json'))
66
- .map((file) => {
67
- const timestamp_match = file.match(/mcp-servers-(\d{4})-(\d{2})-(\d{2})-(\d{2})(\d{2})(\d{2})\.json/);
68
- if (!timestamp_match)
69
- return null;
70
- const [, year, month, day, hour, minute, second] = timestamp_match;
71
- const timestamp = new Date(parseInt(year), parseInt(month) - 1, parseInt(day), parseInt(hour), parseInt(minute), parseInt(second));
72
- return {
73
- filename: file,
74
- timestamp,
75
- path: join(backups_dir, file),
76
- };
77
- })
78
- .filter((backup) => backup !== null)
79
- .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
80
- return backup_files;
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
- throw error;
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
@@ -1,4 +1,5 @@
1
- import { access, readFile, writeFile } from 'node:fs/promises';
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
- const settings_path = get_claude_settings_path();
21
- let existing = {};
22
- try {
23
- const content = await readFile(settings_path, 'utf-8');
24
- existing = JSON.parse(content);
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 profile_config = await load_profile(name);
33
- await write_claude_config(profile_config);
34
- const server_count = Object.keys(profile_config.mcpServers || {}).length;
35
- log.success(`Profile '${name}' applied (${server_count} servers)`);
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 server_count = await save_profile(name);
66
- log.success(`Profile '${name}' saved (${server_count} servers)`);
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
- value: p.name,
90
- label: p.name,
91
- hint: `${p.serverCount} servers`,
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 profile_config = await load_profile(profile_name);
97
- await write_claude_config(profile_config);
98
- const server_count = Object.keys(profile_config.mcpServers || {}).length;
99
- log.success(`Profile '${profile_name}' applied (${server_count} servers)`);
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 server_count = await save_profile(name);
117
- log.success(`Profile '${name}' saved (${server_count} servers)`);
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: 'Enable / Disable plugins',
150
- hint: 'Toggle Claude Code plugins on/off',
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
@@ -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
  */
@@ -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
- export function get_backup_filename() {
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 `mcp-servers-${year}-${month}-${day}-${hour}${minute}${second}.json`;
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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcpick",
3
- "version": "0.0.10",
3
+ "version": "0.0.12",
4
4
  "description": "Dynamic MCP server and plugin configuration manager for Claude Code",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",