mcpick 0.0.9 → 0.0.10
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 +9 -0
- package/dist/cli/commands/add.js +135 -0
- package/dist/cli/commands/backup.js +55 -0
- package/dist/cli/commands/cache.js +181 -0
- package/dist/cli/commands/disable.js +33 -0
- package/dist/cli/commands/enable.js +39 -0
- package/dist/cli/commands/list.js +63 -0
- package/dist/cli/commands/plugins.js +93 -0
- package/dist/cli/commands/profile.js +112 -0
- package/dist/cli/commands/remove.js +31 -0
- package/dist/cli/commands/restore.js +56 -0
- package/dist/cli/index.js +21 -0
- package/dist/cli/output.js +21 -0
- package/dist/commands/manage-cache.js +155 -0
- package/dist/core/plugin-cache.js +259 -0
- package/dist/index.js +33 -4
- package/dist/utils/paths.js +18 -0
- package/package.json +7 -6
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { defineCommand } from 'citty';
|
|
2
|
+
import { write_claude_config } from '../../core/config.js';
|
|
3
|
+
import { list_profiles, load_profile, save_profile, } from '../../core/profile.js';
|
|
4
|
+
import { error, output } from '../output.js';
|
|
5
|
+
const list = defineCommand({
|
|
6
|
+
meta: {
|
|
7
|
+
name: 'list',
|
|
8
|
+
description: 'List all saved profiles',
|
|
9
|
+
},
|
|
10
|
+
args: {
|
|
11
|
+
json: {
|
|
12
|
+
type: 'boolean',
|
|
13
|
+
description: 'Output as JSON',
|
|
14
|
+
default: false,
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
async run({ args }) {
|
|
18
|
+
const profiles = await list_profiles();
|
|
19
|
+
if (args.json) {
|
|
20
|
+
output(profiles, true);
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
if (profiles.length === 0) {
|
|
24
|
+
console.log('No profiles found.');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
for (const p of profiles) {
|
|
28
|
+
console.log(`${p.name} (${p.serverCount} servers)`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
const load = defineCommand({
|
|
34
|
+
meta: {
|
|
35
|
+
name: 'load',
|
|
36
|
+
description: 'Load and apply a saved profile',
|
|
37
|
+
},
|
|
38
|
+
args: {
|
|
39
|
+
name: {
|
|
40
|
+
type: 'positional',
|
|
41
|
+
description: 'Profile name',
|
|
42
|
+
required: true,
|
|
43
|
+
},
|
|
44
|
+
json: {
|
|
45
|
+
type: 'boolean',
|
|
46
|
+
description: 'Output as JSON',
|
|
47
|
+
default: false,
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
async run({ args }) {
|
|
51
|
+
try {
|
|
52
|
+
const config = await load_profile(args.name);
|
|
53
|
+
await write_claude_config(config);
|
|
54
|
+
const server_count = Object.keys(config.mcpServers || {}).length;
|
|
55
|
+
if (args.json) {
|
|
56
|
+
output({
|
|
57
|
+
profile: args.name,
|
|
58
|
+
servers: server_count,
|
|
59
|
+
}, true);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
console.log(`Profile '${args.name}' applied (${server_count} servers)`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
error(err instanceof Error ? err.message : 'Failed to load profile');
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
const save = defineCommand({
|
|
71
|
+
meta: {
|
|
72
|
+
name: 'save',
|
|
73
|
+
description: 'Save current config as a profile',
|
|
74
|
+
},
|
|
75
|
+
args: {
|
|
76
|
+
name: {
|
|
77
|
+
type: 'positional',
|
|
78
|
+
description: 'Profile name',
|
|
79
|
+
required: true,
|
|
80
|
+
},
|
|
81
|
+
json: {
|
|
82
|
+
type: 'boolean',
|
|
83
|
+
description: 'Output as JSON',
|
|
84
|
+
default: false,
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
async run({ args }) {
|
|
88
|
+
try {
|
|
89
|
+
const server_count = await save_profile(args.name);
|
|
90
|
+
if (args.json) {
|
|
91
|
+
output({
|
|
92
|
+
profile: args.name,
|
|
93
|
+
servers: server_count,
|
|
94
|
+
}, true);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
console.log(`Profile '${args.name}' saved (${server_count} servers)`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
error(err instanceof Error ? err.message : 'Failed to save profile');
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
export default defineCommand({
|
|
106
|
+
meta: {
|
|
107
|
+
name: 'profile',
|
|
108
|
+
description: 'Manage MCP server profiles',
|
|
109
|
+
},
|
|
110
|
+
subCommands: { list, load, save },
|
|
111
|
+
});
|
|
112
|
+
//# sourceMappingURL=profile.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { defineCommand } from 'citty';
|
|
2
|
+
import { read_server_registry, write_server_registry, } from '../../core/registry.js';
|
|
3
|
+
import { remove_mcp_via_cli } from '../../utils/claude-cli.js';
|
|
4
|
+
import { error } from '../output.js';
|
|
5
|
+
export default defineCommand({
|
|
6
|
+
meta: {
|
|
7
|
+
name: 'remove',
|
|
8
|
+
description: 'Remove an MCP server from the registry and disable it',
|
|
9
|
+
},
|
|
10
|
+
args: {
|
|
11
|
+
server: {
|
|
12
|
+
type: 'positional',
|
|
13
|
+
description: 'Server name to remove',
|
|
14
|
+
required: true,
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
async run({ args }) {
|
|
18
|
+
const registry = await read_server_registry();
|
|
19
|
+
const index = registry.servers.findIndex((s) => s.name === args.server);
|
|
20
|
+
if (index < 0) {
|
|
21
|
+
error(`Server '${args.server}' not found in registry. Run 'mcpick list' to see available servers.`);
|
|
22
|
+
}
|
|
23
|
+
// Remove from registry
|
|
24
|
+
registry.servers.splice(index, 1);
|
|
25
|
+
await write_server_registry(registry);
|
|
26
|
+
// Also disable via CLI (best effort)
|
|
27
|
+
await remove_mcp_via_cli(args.server);
|
|
28
|
+
console.log(`Removed '${args.server}' from registry`);
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
//# sourceMappingURL=remove.js.map
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { defineCommand } from 'citty';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { write_claude_config } from '../../core/config.js';
|
|
4
|
+
import { list_backups } from '../../core/registry.js';
|
|
5
|
+
import { validate_claude_config } from '../../core/validation.js';
|
|
6
|
+
import { error, output } from '../output.js';
|
|
7
|
+
export default defineCommand({
|
|
8
|
+
meta: {
|
|
9
|
+
name: 'restore',
|
|
10
|
+
description: 'Restore MCP server config from a backup (latest if no file specified)',
|
|
11
|
+
},
|
|
12
|
+
args: {
|
|
13
|
+
file: {
|
|
14
|
+
type: 'positional',
|
|
15
|
+
description: 'Backup filename or path (optional, defaults to latest)',
|
|
16
|
+
required: false,
|
|
17
|
+
},
|
|
18
|
+
json: {
|
|
19
|
+
type: 'boolean',
|
|
20
|
+
description: 'Output as JSON',
|
|
21
|
+
default: false,
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
async run({ args }) {
|
|
25
|
+
const backups = await list_backups();
|
|
26
|
+
if (backups.length === 0) {
|
|
27
|
+
error('No backups found. Run "mcpick backup" first.');
|
|
28
|
+
}
|
|
29
|
+
let backup_path;
|
|
30
|
+
if (args.file) {
|
|
31
|
+
const found = backups.find((b) => b.filename === args.file || b.path === args.file);
|
|
32
|
+
if (!found) {
|
|
33
|
+
error(`Backup '${args.file}' not found. Available: ${backups.map((b) => b.filename).join(', ')}`);
|
|
34
|
+
}
|
|
35
|
+
backup_path = found.path;
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
backup_path = backups[0].path;
|
|
39
|
+
}
|
|
40
|
+
const content = await readFile(backup_path, 'utf-8');
|
|
41
|
+
const parsed = JSON.parse(content);
|
|
42
|
+
const config = validate_claude_config(parsed);
|
|
43
|
+
await write_claude_config(config);
|
|
44
|
+
const server_count = Object.keys(config.mcpServers || {}).length;
|
|
45
|
+
if (args.json) {
|
|
46
|
+
output({
|
|
47
|
+
restored: backup_path,
|
|
48
|
+
servers: server_count,
|
|
49
|
+
}, true);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
console.log(`Restored from ${backup_path} (${server_count} servers)`);
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
//# sourceMappingURL=restore.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { defineCommand, runMain } from 'citty';
|
|
2
|
+
const main = defineCommand({
|
|
3
|
+
meta: {
|
|
4
|
+
name: 'mcpick',
|
|
5
|
+
description: 'MCP Server Configuration Manager',
|
|
6
|
+
},
|
|
7
|
+
subCommands: {
|
|
8
|
+
list: () => import('./commands/list.js').then((m) => m.default),
|
|
9
|
+
enable: () => import('./commands/enable.js').then((m) => m.default),
|
|
10
|
+
disable: () => import('./commands/disable.js').then((m) => m.default),
|
|
11
|
+
remove: () => import('./commands/remove.js').then((m) => m.default),
|
|
12
|
+
add: () => import('./commands/add.js').then((m) => m.default),
|
|
13
|
+
backup: () => import('./commands/backup.js').then((m) => m.default),
|
|
14
|
+
restore: () => import('./commands/restore.js').then((m) => m.default),
|
|
15
|
+
profile: () => import('./commands/profile.js').then((m) => m.default),
|
|
16
|
+
plugins: () => import('./commands/plugins.js').then((m) => m.default),
|
|
17
|
+
cache: () => import('./commands/cache.js').then((m) => m.default),
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
export const run = () => runMain(main);
|
|
21
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function output(data, json) {
|
|
2
|
+
if (json) {
|
|
3
|
+
console.log(JSON.stringify(data, null, 2));
|
|
4
|
+
}
|
|
5
|
+
else if (typeof data === 'string') {
|
|
6
|
+
console.log(data);
|
|
7
|
+
}
|
|
8
|
+
else if (Array.isArray(data)) {
|
|
9
|
+
for (const item of data) {
|
|
10
|
+
console.log(item);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
console.log(data);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function error(message) {
|
|
18
|
+
console.error(`error: ${message}`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=output.js.map
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { confirm, isCancel, log, multiselect, note, select, } from '@clack/prompts';
|
|
2
|
+
import { clean_orphaned_versions, clear_plugin_caches, get_cached_plugins_info, refresh_all_marketplaces, } from '../core/plugin-cache.js';
|
|
3
|
+
function format_status_line(p) {
|
|
4
|
+
const markers = [];
|
|
5
|
+
if (p.isVersionStale) {
|
|
6
|
+
markers.push(`version: ${p.installedVersion} → ${p.latestVersion}`);
|
|
7
|
+
}
|
|
8
|
+
if (p.isShaStale) {
|
|
9
|
+
markers.push('commits behind');
|
|
10
|
+
}
|
|
11
|
+
if (p.orphanedVersions.length > 0) {
|
|
12
|
+
markers.push(`${p.orphanedVersions.length} orphaned`);
|
|
13
|
+
}
|
|
14
|
+
const status = markers.length > 0
|
|
15
|
+
? `[stale: ${markers.join(', ')}]`
|
|
16
|
+
: '[up to date]';
|
|
17
|
+
return `${p.name}@${p.marketplace} v${p.installedVersion} ${status}`;
|
|
18
|
+
}
|
|
19
|
+
async function handle_status() {
|
|
20
|
+
const plugins = await get_cached_plugins_info();
|
|
21
|
+
if (plugins.length === 0) {
|
|
22
|
+
log.info('No cached plugins found.');
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const lines = plugins.map(format_status_line).join('\n');
|
|
26
|
+
note(lines, 'Plugin Cache Status');
|
|
27
|
+
}
|
|
28
|
+
async function handle_clear() {
|
|
29
|
+
const plugins = await get_cached_plugins_info();
|
|
30
|
+
if (plugins.length === 0) {
|
|
31
|
+
log.info('No cached plugins to clear.');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const selected = await multiselect({
|
|
35
|
+
message: 'Select plugins to clear cache for:',
|
|
36
|
+
options: plugins.map((p) => {
|
|
37
|
+
const stale = p.isVersionStale || p.isShaStale;
|
|
38
|
+
return {
|
|
39
|
+
value: p.key,
|
|
40
|
+
label: `${p.name}@${p.marketplace}`,
|
|
41
|
+
hint: stale
|
|
42
|
+
? `v${p.installedVersion} → ${p.latestVersion ?? 'unknown'} (stale)`
|
|
43
|
+
: `v${p.installedVersion}`,
|
|
44
|
+
};
|
|
45
|
+
}),
|
|
46
|
+
initialValues: plugins
|
|
47
|
+
.filter((p) => p.isVersionStale || p.isShaStale)
|
|
48
|
+
.map((p) => p.key),
|
|
49
|
+
});
|
|
50
|
+
if (isCancel(selected) || selected.length === 0)
|
|
51
|
+
return;
|
|
52
|
+
const should_clear = await confirm({
|
|
53
|
+
message: `Clear cache for ${selected.length} plugin(s)? This will also refresh the marketplace.`,
|
|
54
|
+
});
|
|
55
|
+
if (isCancel(should_clear) || !should_clear)
|
|
56
|
+
return;
|
|
57
|
+
const result = await clear_plugin_caches(selected);
|
|
58
|
+
for (const key of result.cleared) {
|
|
59
|
+
log.success(`Cleared: ${key}`);
|
|
60
|
+
}
|
|
61
|
+
for (const err of result.errors) {
|
|
62
|
+
log.error(`Error: ${err}`);
|
|
63
|
+
}
|
|
64
|
+
if (result.cleared.length > 0) {
|
|
65
|
+
note('Run /reload-plugins in Claude Code or restart your session to apply changes.', 'Next Steps');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function handle_clean_orphaned() {
|
|
69
|
+
const should_clean = await confirm({
|
|
70
|
+
message: 'Remove all orphaned plugin version directories?',
|
|
71
|
+
});
|
|
72
|
+
if (isCancel(should_clean) || !should_clean)
|
|
73
|
+
return;
|
|
74
|
+
const result = await clean_orphaned_versions();
|
|
75
|
+
if (result.cleaned === 0) {
|
|
76
|
+
log.info('No orphaned versions found.');
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
for (const p of result.paths) {
|
|
80
|
+
log.success(`Removed: ${p}`);
|
|
81
|
+
}
|
|
82
|
+
log.info(`Cleaned ${result.cleaned} orphaned version(s).`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async function handle_refresh() {
|
|
86
|
+
const should_refresh = await confirm({
|
|
87
|
+
message: 'Refresh all marketplace clones (git pull)?',
|
|
88
|
+
});
|
|
89
|
+
if (isCancel(should_refresh) || !should_refresh)
|
|
90
|
+
return;
|
|
91
|
+
const results = await refresh_all_marketplaces();
|
|
92
|
+
if (results.size === 0) {
|
|
93
|
+
log.info('No marketplaces configured.');
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
for (const [name, result] of results) {
|
|
97
|
+
if (result.success) {
|
|
98
|
+
log.success(`${name}: refreshed`);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
log.error(`${name}: ${result.error}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
export async function manage_cache() {
|
|
106
|
+
while (true) {
|
|
107
|
+
const action = await select({
|
|
108
|
+
message: 'Plugin cache management:',
|
|
109
|
+
options: [
|
|
110
|
+
{
|
|
111
|
+
value: 'status',
|
|
112
|
+
label: 'View cache status',
|
|
113
|
+
hint: 'Show plugins with staleness info',
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
value: 'clear',
|
|
117
|
+
label: 'Clear plugin caches',
|
|
118
|
+
hint: 'Refresh marketplace + clear selected caches',
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
value: 'clean-orphaned',
|
|
122
|
+
label: 'Clean orphaned versions',
|
|
123
|
+
hint: 'Remove old version directories',
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
value: 'refresh',
|
|
127
|
+
label: 'Refresh marketplaces',
|
|
128
|
+
hint: 'Git pull all marketplace clones',
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
value: 'back',
|
|
132
|
+
label: 'Back',
|
|
133
|
+
hint: 'Return to main menu',
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
});
|
|
137
|
+
if (isCancel(action) || action === 'back')
|
|
138
|
+
return;
|
|
139
|
+
switch (action) {
|
|
140
|
+
case 'status':
|
|
141
|
+
await handle_status();
|
|
142
|
+
break;
|
|
143
|
+
case 'clear':
|
|
144
|
+
await handle_clear();
|
|
145
|
+
break;
|
|
146
|
+
case 'clean-orphaned':
|
|
147
|
+
await handle_clean_orphaned();
|
|
148
|
+
break;
|
|
149
|
+
case 'refresh':
|
|
150
|
+
await handle_refresh();
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=manage-cache.js.map
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { exec } from 'node:child_process';
|
|
2
|
+
import { readdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
import { get_installed_plugins_path, get_known_marketplaces_path, get_marketplace_manifest_path, get_plugin_cache_dir, } from '../utils/paths.js';
|
|
6
|
+
const execAsync = promisify(exec);
|
|
7
|
+
const EMPTY_INSTALLED = {
|
|
8
|
+
version: 2,
|
|
9
|
+
plugins: {},
|
|
10
|
+
};
|
|
11
|
+
// --- Data reading ---
|
|
12
|
+
export async function read_installed_plugins() {
|
|
13
|
+
try {
|
|
14
|
+
const content = await readFile(get_installed_plugins_path(), 'utf-8');
|
|
15
|
+
return JSON.parse(content);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return { ...EMPTY_INSTALLED, plugins: {} };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export async function write_installed_plugins(data) {
|
|
22
|
+
await writeFile(get_installed_plugins_path(), JSON.stringify(data, null, 2), 'utf-8');
|
|
23
|
+
}
|
|
24
|
+
export async function read_known_marketplaces() {
|
|
25
|
+
try {
|
|
26
|
+
const content = await readFile(get_known_marketplaces_path(), 'utf-8');
|
|
27
|
+
return JSON.parse(content);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export async function read_marketplace_manifest(name) {
|
|
34
|
+
try {
|
|
35
|
+
const content = await readFile(get_marketplace_manifest_path(name), 'utf-8');
|
|
36
|
+
return JSON.parse(content);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// --- Git operations ---
|
|
43
|
+
async function get_marketplace_head_sha(marketplace_path) {
|
|
44
|
+
try {
|
|
45
|
+
const { stdout } = await execAsync(`git -C ${JSON.stringify(marketplace_path)} rev-parse HEAD`, { timeout: 10_000 });
|
|
46
|
+
return stdout.trim() || null;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export async function refresh_marketplace(name, marketplace) {
|
|
53
|
+
const dir = marketplace.installLocation;
|
|
54
|
+
try {
|
|
55
|
+
await execAsync(`git -C ${JSON.stringify(dir)} pull --ff-only`, {
|
|
56
|
+
timeout: 30_000,
|
|
57
|
+
});
|
|
58
|
+
return { success: true };
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
62
|
+
return { success: false, error: `${name}: ${message}` };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export async function refresh_all_marketplaces() {
|
|
66
|
+
const marketplaces = await read_known_marketplaces();
|
|
67
|
+
const results = new Map();
|
|
68
|
+
for (const [name, info] of Object.entries(marketplaces)) {
|
|
69
|
+
results.set(name, await refresh_marketplace(name, info));
|
|
70
|
+
}
|
|
71
|
+
return results;
|
|
72
|
+
}
|
|
73
|
+
// --- Orphaned version detection ---
|
|
74
|
+
async function find_orphaned_versions(marketplace, plugin_name) {
|
|
75
|
+
const cache_dir = get_plugin_cache_dir();
|
|
76
|
+
const plugin_dir = join(cache_dir, marketplace, plugin_name);
|
|
77
|
+
const orphaned = [];
|
|
78
|
+
try {
|
|
79
|
+
const versions = await readdir(plugin_dir, {
|
|
80
|
+
withFileTypes: true,
|
|
81
|
+
});
|
|
82
|
+
for (const entry of versions) {
|
|
83
|
+
if (!entry.isDirectory())
|
|
84
|
+
continue;
|
|
85
|
+
try {
|
|
86
|
+
const marker = join(plugin_dir, entry.name, '.orphaned_at');
|
|
87
|
+
await readFile(marker);
|
|
88
|
+
orphaned.push(entry.name);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// No .orphaned_at marker — not orphaned
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// Plugin dir doesn't exist
|
|
97
|
+
}
|
|
98
|
+
return orphaned;
|
|
99
|
+
}
|
|
100
|
+
// --- Staleness analysis ---
|
|
101
|
+
function parse_plugin_key(key) {
|
|
102
|
+
const at_index = key.lastIndexOf('@');
|
|
103
|
+
return {
|
|
104
|
+
name: at_index > 0 ? key.substring(0, at_index) : key,
|
|
105
|
+
marketplace: at_index > 0 ? key.substring(at_index + 1) : 'unknown',
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
export async function get_cached_plugins_info() {
|
|
109
|
+
const installed = await read_installed_plugins();
|
|
110
|
+
const marketplaces = await read_known_marketplaces();
|
|
111
|
+
// Load all marketplace manifests
|
|
112
|
+
const manifests = new Map();
|
|
113
|
+
for (const name of Object.keys(marketplaces)) {
|
|
114
|
+
const manifest = await read_marketplace_manifest(name);
|
|
115
|
+
if (manifest)
|
|
116
|
+
manifests.set(name, manifest);
|
|
117
|
+
}
|
|
118
|
+
// Get HEAD SHAs for each marketplace clone
|
|
119
|
+
const sha_cache = new Map();
|
|
120
|
+
for (const [name, info] of Object.entries(marketplaces)) {
|
|
121
|
+
sha_cache.set(name, await get_marketplace_head_sha(info.installLocation));
|
|
122
|
+
}
|
|
123
|
+
const results = [];
|
|
124
|
+
for (const [key, entries] of Object.entries(installed.plugins)) {
|
|
125
|
+
const entry = entries[0];
|
|
126
|
+
if (!entry)
|
|
127
|
+
continue;
|
|
128
|
+
const { name, marketplace } = parse_plugin_key(key);
|
|
129
|
+
// Version comparison
|
|
130
|
+
const manifest = manifests.get(marketplace);
|
|
131
|
+
const manifest_plugin = manifest?.plugins.find((p) => p.name === name);
|
|
132
|
+
const latest_version = manifest_plugin?.version ?? null;
|
|
133
|
+
const is_version_stale = latest_version !== null && latest_version !== entry.version;
|
|
134
|
+
// SHA comparison
|
|
135
|
+
const remote_sha = sha_cache.get(marketplace) ?? null;
|
|
136
|
+
const is_sha_stale = remote_sha !== null &&
|
|
137
|
+
entry.gitCommitSha !== '' &&
|
|
138
|
+
remote_sha !== entry.gitCommitSha;
|
|
139
|
+
// Orphaned versions
|
|
140
|
+
const orphaned = await find_orphaned_versions(marketplace, name);
|
|
141
|
+
results.push({
|
|
142
|
+
key,
|
|
143
|
+
name,
|
|
144
|
+
marketplace,
|
|
145
|
+
installedVersion: entry.version,
|
|
146
|
+
latestVersion: latest_version,
|
|
147
|
+
installedSha: entry.gitCommitSha,
|
|
148
|
+
remoteSha: remote_sha,
|
|
149
|
+
isVersionStale: is_version_stale,
|
|
150
|
+
isShaStale: is_sha_stale,
|
|
151
|
+
orphanedVersions: orphaned,
|
|
152
|
+
installPath: entry.installPath,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return results;
|
|
156
|
+
}
|
|
157
|
+
// --- Cache clearing ---
|
|
158
|
+
function is_safe_cache_path(path) {
|
|
159
|
+
const cache_dir = resolve(get_plugin_cache_dir());
|
|
160
|
+
const target = resolve(path);
|
|
161
|
+
return target.startsWith(cache_dir + '/');
|
|
162
|
+
}
|
|
163
|
+
export async function clear_plugin_caches(keys) {
|
|
164
|
+
const installed = await read_installed_plugins();
|
|
165
|
+
const marketplaces = await read_known_marketplaces();
|
|
166
|
+
const cleared = [];
|
|
167
|
+
const errors = [];
|
|
168
|
+
// Collect unique marketplaces to refresh
|
|
169
|
+
const marketplace_names = new Set();
|
|
170
|
+
for (const key of keys) {
|
|
171
|
+
const { marketplace } = parse_plugin_key(key);
|
|
172
|
+
marketplace_names.add(marketplace);
|
|
173
|
+
}
|
|
174
|
+
// Refresh relevant marketplaces first
|
|
175
|
+
for (const mkt_name of marketplace_names) {
|
|
176
|
+
const mkt_info = marketplaces[mkt_name];
|
|
177
|
+
if (mkt_info) {
|
|
178
|
+
const result = await refresh_marketplace(mkt_name, mkt_info);
|
|
179
|
+
if (!result.success) {
|
|
180
|
+
errors.push(`Marketplace refresh failed: ${result.error}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// Delete cache dirs and remove from installed_plugins.json
|
|
185
|
+
const cache_dir = get_plugin_cache_dir();
|
|
186
|
+
for (const key of keys) {
|
|
187
|
+
const { name, marketplace } = parse_plugin_key(key);
|
|
188
|
+
const plugin_cache_path = join(cache_dir, marketplace, name);
|
|
189
|
+
if (!is_safe_cache_path(plugin_cache_path)) {
|
|
190
|
+
errors.push(`Unsafe path, skipped: ${plugin_cache_path}`);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
await rm(plugin_cache_path, {
|
|
195
|
+
recursive: true,
|
|
196
|
+
force: true,
|
|
197
|
+
});
|
|
198
|
+
delete installed.plugins[key];
|
|
199
|
+
cleared.push(key);
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
const msg = err instanceof Error ? err.message : 'Unknown error';
|
|
203
|
+
errors.push(`Failed to clear ${key}: ${msg}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// Write back updated installed_plugins.json
|
|
207
|
+
await write_installed_plugins(installed);
|
|
208
|
+
return { cleared, errors };
|
|
209
|
+
}
|
|
210
|
+
// --- Orphaned cleanup ---
|
|
211
|
+
export async function clean_orphaned_versions() {
|
|
212
|
+
const cache_dir = get_plugin_cache_dir();
|
|
213
|
+
const cleaned_paths = [];
|
|
214
|
+
try {
|
|
215
|
+
const marketplaces = await readdir(cache_dir, {
|
|
216
|
+
withFileTypes: true,
|
|
217
|
+
});
|
|
218
|
+
for (const mkt of marketplaces) {
|
|
219
|
+
if (!mkt.isDirectory())
|
|
220
|
+
continue;
|
|
221
|
+
const mkt_path = join(cache_dir, mkt.name);
|
|
222
|
+
const plugins = await readdir(mkt_path, {
|
|
223
|
+
withFileTypes: true,
|
|
224
|
+
});
|
|
225
|
+
for (const plugin of plugins) {
|
|
226
|
+
if (!plugin.isDirectory())
|
|
227
|
+
continue;
|
|
228
|
+
const plugin_path = join(mkt_path, plugin.name);
|
|
229
|
+
const versions = await readdir(plugin_path, {
|
|
230
|
+
withFileTypes: true,
|
|
231
|
+
});
|
|
232
|
+
for (const version of versions) {
|
|
233
|
+
if (!version.isDirectory())
|
|
234
|
+
continue;
|
|
235
|
+
const version_path = join(plugin_path, version.name);
|
|
236
|
+
try {
|
|
237
|
+
await readFile(join(version_path, '.orphaned_at'));
|
|
238
|
+
// Has orphaned marker — safe to delete
|
|
239
|
+
if (is_safe_cache_path(version_path)) {
|
|
240
|
+
await rm(version_path, {
|
|
241
|
+
recursive: true,
|
|
242
|
+
force: true,
|
|
243
|
+
});
|
|
244
|
+
cleaned_paths.push(`${mkt.name}/${plugin.name}/${version.name}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
// No marker — keep it
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
// Cache dir doesn't exist
|
|
256
|
+
}
|
|
257
|
+
return { cleaned: cleaned_paths.length, paths: cleaned_paths };
|
|
258
|
+
}
|
|
259
|
+
//# sourceMappingURL=plugin-cache.js.map
|