@shnitzel/plugscout 0.3.10 → 0.3.11
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.
|
@@ -1,24 +1,111 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import os from 'node:os';
|
|
4
|
-
const
|
|
4
|
+
const PLUGSCOUT_MCP_STDIO = { command: 'npx', args: ['plugscout', 'mcp'] };
|
|
5
|
+
const PLUGSCOUT_MCP_ZED = { command: { path: 'npx', args: ['plugscout', 'mcp'] } };
|
|
6
|
+
function claudeDesktopConfigPath() {
|
|
7
|
+
if (process.platform === 'win32') {
|
|
8
|
+
return path.join(process.env['APPDATA'] ?? os.homedir(), 'Claude', 'claude_desktop_config.json');
|
|
9
|
+
}
|
|
10
|
+
if (process.platform === 'darwin') {
|
|
11
|
+
return path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
|
|
12
|
+
}
|
|
13
|
+
return path.join(os.homedir(), '.config', 'claude-desktop', 'claude_desktop_config.json');
|
|
14
|
+
}
|
|
15
|
+
function openCodeConfigPath() {
|
|
16
|
+
if (process.platform === 'win32') {
|
|
17
|
+
return path.join(process.env['APPDATA'] ?? os.homedir(), 'opencode', 'config.json');
|
|
18
|
+
}
|
|
19
|
+
return path.join(os.homedir(), '.config', 'opencode', 'config.json');
|
|
20
|
+
}
|
|
21
|
+
export const CLIENT_DEFS = {
|
|
22
|
+
cursor: {
|
|
23
|
+
label: 'Cursor IDE',
|
|
24
|
+
supportsProjectScope: true,
|
|
25
|
+
getConfigPath(scope) {
|
|
26
|
+
return scope === 'project'
|
|
27
|
+
? path.join(process.cwd(), '.cursor', 'mcp.json')
|
|
28
|
+
: path.join(os.homedir(), '.cursor', 'mcp.json');
|
|
29
|
+
},
|
|
30
|
+
containerPath: ['mcpServers'],
|
|
31
|
+
entryValue: PLUGSCOUT_MCP_STDIO,
|
|
32
|
+
},
|
|
33
|
+
gemini: {
|
|
34
|
+
label: 'Gemini CLI',
|
|
35
|
+
supportsProjectScope: false,
|
|
36
|
+
getConfigPath() {
|
|
37
|
+
return path.join(os.homedir(), '.gemini', 'settings.json');
|
|
38
|
+
},
|
|
39
|
+
containerPath: ['mcpServers'],
|
|
40
|
+
entryValue: PLUGSCOUT_MCP_STDIO,
|
|
41
|
+
},
|
|
42
|
+
'claude-desktop': {
|
|
43
|
+
label: 'Claude Desktop',
|
|
44
|
+
supportsProjectScope: false,
|
|
45
|
+
getConfigPath() {
|
|
46
|
+
return claudeDesktopConfigPath();
|
|
47
|
+
},
|
|
48
|
+
containerPath: ['mcpServers'],
|
|
49
|
+
entryValue: PLUGSCOUT_MCP_STDIO,
|
|
50
|
+
},
|
|
51
|
+
windsurf: {
|
|
52
|
+
label: 'Windsurf',
|
|
53
|
+
supportsProjectScope: false,
|
|
54
|
+
getConfigPath() {
|
|
55
|
+
return path.join(os.homedir(), '.codeium', 'windsurf', 'mcp_config.json');
|
|
56
|
+
},
|
|
57
|
+
containerPath: ['mcpServers'],
|
|
58
|
+
entryValue: PLUGSCOUT_MCP_STDIO,
|
|
59
|
+
},
|
|
60
|
+
opencode: {
|
|
61
|
+
label: 'OpenCode',
|
|
62
|
+
supportsProjectScope: false,
|
|
63
|
+
getConfigPath() {
|
|
64
|
+
return openCodeConfigPath();
|
|
65
|
+
},
|
|
66
|
+
containerPath: ['mcp'],
|
|
67
|
+
entryValue: PLUGSCOUT_MCP_STDIO,
|
|
68
|
+
},
|
|
69
|
+
zed: {
|
|
70
|
+
label: 'Zed',
|
|
71
|
+
supportsProjectScope: false,
|
|
72
|
+
getConfigPath() {
|
|
73
|
+
return path.join(os.homedir(), '.config', 'zed', 'settings.json');
|
|
74
|
+
},
|
|
75
|
+
containerPath: ['context_servers'],
|
|
76
|
+
entryValue: PLUGSCOUT_MCP_ZED,
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
export const VALID_CLIENT_KINDS = Object.keys(CLIENT_DEFS);
|
|
5
80
|
export function getConfigPath(client, scope) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
81
|
+
return CLIENT_DEFS[client].getConfigPath(scope);
|
|
82
|
+
}
|
|
83
|
+
function navigateContainer(obj, keys) {
|
|
84
|
+
let current = obj;
|
|
85
|
+
for (const key of keys) {
|
|
86
|
+
const next = current[key];
|
|
87
|
+
if (next === undefined || next === null || typeof next !== 'object' || Array.isArray(next)) {
|
|
88
|
+
current[key] = {};
|
|
9
89
|
}
|
|
10
|
-
|
|
90
|
+
current = current[key];
|
|
11
91
|
}
|
|
12
|
-
return
|
|
92
|
+
return current;
|
|
13
93
|
}
|
|
14
94
|
export async function getClientMcpConfigStatus(client, scope) {
|
|
15
|
-
const
|
|
95
|
+
const def = CLIENT_DEFS[client];
|
|
96
|
+
const configPath = def.getConfigPath(scope);
|
|
16
97
|
try {
|
|
17
98
|
const raw = await fs.readFile(configPath, 'utf8');
|
|
18
99
|
const config = JSON.parse(raw);
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
100
|
+
let container = config;
|
|
101
|
+
for (const key of def.containerPath) {
|
|
102
|
+
const next = container[key];
|
|
103
|
+
if (next === undefined || typeof next !== 'object' || next === null || Array.isArray(next)) {
|
|
104
|
+
return { configured: false, configPath };
|
|
105
|
+
}
|
|
106
|
+
container = next;
|
|
107
|
+
}
|
|
108
|
+
return { configured: !!container['plugscout'], configPath };
|
|
22
109
|
}
|
|
23
110
|
catch {
|
|
24
111
|
return { configured: false, configPath };
|
|
@@ -26,8 +113,9 @@ export async function getClientMcpConfigStatus(client, scope) {
|
|
|
26
113
|
}
|
|
27
114
|
export async function writeClientMcpConfig(options) {
|
|
28
115
|
const { client, force = false } = options;
|
|
29
|
-
const
|
|
30
|
-
const
|
|
116
|
+
const def = CLIENT_DEFS[client];
|
|
117
|
+
const scope = def.supportsProjectScope ? options.scope : 'user';
|
|
118
|
+
const configPath = def.getConfigPath(scope);
|
|
31
119
|
let existing = {};
|
|
32
120
|
try {
|
|
33
121
|
const raw = await fs.readFile(configPath, 'utf8');
|
|
@@ -36,25 +124,21 @@ export async function writeClientMcpConfig(options) {
|
|
|
36
124
|
catch {
|
|
37
125
|
// file doesn't exist yet — start empty
|
|
38
126
|
}
|
|
39
|
-
const
|
|
40
|
-
const current =
|
|
127
|
+
const container = navigateContainer(existing, def.containerPath);
|
|
128
|
+
const current = container['plugscout'];
|
|
41
129
|
if (current !== undefined) {
|
|
42
|
-
const isSame = JSON.stringify(current) === JSON.stringify(
|
|
43
|
-
if (isSame)
|
|
130
|
+
const isSame = JSON.stringify(current) === JSON.stringify(def.entryValue);
|
|
131
|
+
if (isSame)
|
|
44
132
|
return { status: 'already-configured', configPath };
|
|
45
|
-
}
|
|
46
133
|
if (!force) {
|
|
47
134
|
throw new Error(`plugscout already exists in ${configPath} with a different value. Use --force to overwrite.`);
|
|
48
135
|
}
|
|
49
136
|
}
|
|
50
|
-
|
|
51
|
-
...existing,
|
|
52
|
-
mcpServers: { ...mcpServers, plugscout: PLUGSCOUT_MCP_VALUE }
|
|
53
|
-
};
|
|
137
|
+
container['plugscout'] = def.entryValue;
|
|
54
138
|
const dir = path.dirname(configPath);
|
|
55
139
|
await fs.mkdir(dir, { recursive: true });
|
|
56
140
|
const tmpPath = `${configPath}.plugscout.tmp`;
|
|
57
|
-
await fs.writeFile(tmpPath, `${JSON.stringify(
|
|
141
|
+
await fs.writeFile(tmpPath, `${JSON.stringify(existing, null, 2)}\n`, 'utf8');
|
|
58
142
|
await fs.rename(tmpPath, configPath);
|
|
59
143
|
return { status: 'written', configPath };
|
|
60
144
|
}
|
|
@@ -5,7 +5,7 @@ import fs from 'node:fs/promises';
|
|
|
5
5
|
import { getStaleRegistries, loadSyncState } from '../../catalog/sync-state.js';
|
|
6
6
|
import { loadCatalogItems } from '../../catalog/repository.js';
|
|
7
7
|
import { hasLegacySkillSh, resolveSkillsRuntime } from '../../install/dependencies.js';
|
|
8
|
-
import { getClientMcpConfigStatus } from './client-setup.js';
|
|
8
|
+
import { getClientMcpConfigStatus, CLIENT_DEFS } from './client-setup.js';
|
|
9
9
|
export async function runDoctorChecks(projectPath = '.') {
|
|
10
10
|
const checks = [];
|
|
11
11
|
checks.push(checkSkillsRuntime());
|
|
@@ -88,6 +88,64 @@ export async function runDoctorChecks(projectPath = '.') {
|
|
|
88
88
|
catch {
|
|
89
89
|
checks.push({ name: 'Gemini MCP config', status: 'warn', message: 'Could not read Gemini MCP config', suggestion: 'Run: plugscout client setup --client gemini' });
|
|
90
90
|
}
|
|
91
|
+
// Claude Desktop check
|
|
92
|
+
const claudeDesktopConfigPath = CLIENT_DEFS['claude-desktop'].getConfigPath('user');
|
|
93
|
+
const claudeDesktopPresent = spawnSync('which', ['claude'], { encoding: 'utf8' }).status === 0 ||
|
|
94
|
+
await fs.access(path.dirname(claudeDesktopConfigPath)).then(() => true).catch(() => false) ||
|
|
95
|
+
await fs.access(path.join('/', 'Applications', 'Claude.app')).then(() => true).catch(() => false);
|
|
96
|
+
checks.push(claudeDesktopPresent
|
|
97
|
+
? { name: 'Claude Desktop', status: 'pass', message: 'Claude Desktop detected' }
|
|
98
|
+
: { name: 'Claude Desktop', status: 'warn', message: 'Claude Desktop not detected', suggestion: 'Install from https://claude.ai/download' });
|
|
99
|
+
// Claude Desktop MCP config check
|
|
100
|
+
try {
|
|
101
|
+
const claudeStatus = await getClientMcpConfigStatus('claude-desktop', 'user');
|
|
102
|
+
checks.push(claudeStatus.configured
|
|
103
|
+
? { name: 'Claude Desktop MCP', status: 'pass', message: `plugscout wired in ${claudeStatus.configPath}` }
|
|
104
|
+
: { name: 'Claude Desktop MCP', status: 'warn', message: 'plugscout not in Claude Desktop config', suggestion: 'Run: plugscout client setup --client claude-desktop' });
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
checks.push({ name: 'Claude Desktop MCP', status: 'warn', message: 'Could not read Claude Desktop config', suggestion: 'Run: plugscout client setup --client claude-desktop' });
|
|
108
|
+
}
|
|
109
|
+
// Windsurf check
|
|
110
|
+
checks.push(checkBinary('windsurf', { suggestion: 'Install Windsurf from https://windsurf.ai' }));
|
|
111
|
+
// Windsurf MCP config check
|
|
112
|
+
try {
|
|
113
|
+
const windsurfStatus = await getClientMcpConfigStatus('windsurf', 'user');
|
|
114
|
+
checks.push(windsurfStatus.configured
|
|
115
|
+
? { name: 'Windsurf MCP config', status: 'pass', message: `plugscout wired in ${windsurfStatus.configPath}` }
|
|
116
|
+
: { name: 'Windsurf MCP config', status: 'warn', message: 'plugscout not in Windsurf MCP config', suggestion: 'Run: plugscout client setup --client windsurf' });
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
checks.push({ name: 'Windsurf MCP config', status: 'warn', message: 'Could not read Windsurf MCP config', suggestion: 'Run: plugscout client setup --client windsurf' });
|
|
120
|
+
}
|
|
121
|
+
// OpenCode check
|
|
122
|
+
checks.push(checkBinary('opencode', { suggestion: 'Install OpenCode: npm install -g opencode-ai' }));
|
|
123
|
+
// OpenCode MCP config check
|
|
124
|
+
try {
|
|
125
|
+
const opencodeStatus = await getClientMcpConfigStatus('opencode', 'user');
|
|
126
|
+
checks.push(opencodeStatus.configured
|
|
127
|
+
? { name: 'OpenCode MCP config', status: 'pass', message: `plugscout wired in ${opencodeStatus.configPath}` }
|
|
128
|
+
: { name: 'OpenCode MCP config', status: 'warn', message: 'plugscout not in OpenCode config', suggestion: 'Run: plugscout client setup --client opencode' });
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
checks.push({ name: 'OpenCode MCP config', status: 'warn', message: 'Could not read OpenCode config', suggestion: 'Run: plugscout client setup --client opencode' });
|
|
132
|
+
}
|
|
133
|
+
// Zed check
|
|
134
|
+
const zedInstalled = spawnSync('which', ['zed'], { encoding: 'utf8' }).status === 0 ||
|
|
135
|
+
await fs.access(path.join('/', 'Applications', 'Zed.app')).then(() => true).catch(() => false);
|
|
136
|
+
checks.push(zedInstalled
|
|
137
|
+
? { name: 'Zed', status: 'pass', message: 'Zed detected' }
|
|
138
|
+
: { name: 'Zed', status: 'warn', message: 'Zed not detected', suggestion: 'Install Zed from https://zed.dev' });
|
|
139
|
+
// Zed MCP config check
|
|
140
|
+
try {
|
|
141
|
+
const zedStatus = await getClientMcpConfigStatus('zed', 'user');
|
|
142
|
+
checks.push(zedStatus.configured
|
|
143
|
+
? { name: 'Zed MCP config', status: 'pass', message: `plugscout wired in ${zedStatus.configPath}` }
|
|
144
|
+
: { name: 'Zed MCP config', status: 'warn', message: 'plugscout not in Zed settings', suggestion: 'Run: plugscout client setup --client zed' });
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
checks.push({ name: 'Zed MCP config', status: 'warn', message: 'Could not read Zed settings', suggestion: 'Run: plugscout client setup --client zed' });
|
|
148
|
+
}
|
|
91
149
|
return checks;
|
|
92
150
|
}
|
|
93
151
|
function checkSkillsRuntime() {
|
|
@@ -860,28 +860,33 @@ async function handleQuarantine(args) {
|
|
|
860
860
|
async function handleClient(args) {
|
|
861
861
|
const subcommand = args[0];
|
|
862
862
|
if (subcommand !== 'setup') {
|
|
863
|
-
throw new Error('Usage: client setup --client cursor|gemini [--scope user|project] [--force]');
|
|
863
|
+
throw new Error('Usage: client setup --client cursor|gemini|claude-desktop|windsurf|opencode|zed [--scope user|project] [--force]');
|
|
864
864
|
}
|
|
865
|
+
const { writeClientMcpConfig, CLIENT_DEFS, VALID_CLIENT_KINDS } = await import('./client-setup.js');
|
|
865
866
|
const clientFlag = readFlag(args, '--client');
|
|
866
|
-
if (clientFlag
|
|
867
|
-
throw new Error(
|
|
867
|
+
if (!clientFlag || !VALID_CLIENT_KINDS.includes(clientFlag)) {
|
|
868
|
+
throw new Error(`Usage: client setup --client ${VALID_CLIENT_KINDS.join('|')}`);
|
|
868
869
|
}
|
|
869
870
|
const scopeFlag = readFlag(args, '--scope') ?? 'user';
|
|
870
871
|
if (scopeFlag !== 'user' && scopeFlag !== 'project') {
|
|
871
872
|
throw new Error('--scope must be user or project');
|
|
872
873
|
}
|
|
873
|
-
|
|
874
|
-
|
|
874
|
+
const def = CLIENT_DEFS[clientFlag];
|
|
875
|
+
if (!def.supportsProjectScope && scopeFlag === 'project') {
|
|
876
|
+
logger.warn(`${def.label} only supports user scope; falling back to user scope.`);
|
|
875
877
|
}
|
|
876
878
|
const force = hasFlag(args, '--force');
|
|
877
|
-
const
|
|
878
|
-
|
|
879
|
+
const result = await writeClientMcpConfig({
|
|
880
|
+
client: clientFlag,
|
|
881
|
+
scope: scopeFlag,
|
|
882
|
+
force
|
|
883
|
+
});
|
|
879
884
|
if (result.status === 'already-configured') {
|
|
880
885
|
console.log(`plugscout already configured in ${result.configPath}`);
|
|
881
886
|
}
|
|
882
887
|
else {
|
|
883
888
|
console.log(`plugscout MCP config written: ${result.configPath}`);
|
|
884
|
-
printHint(`Restart ${
|
|
889
|
+
printHint(`Restart ${def.label} for the change to take effect.`);
|
|
885
890
|
}
|
|
886
891
|
}
|
|
887
892
|
async function handleUpgrade(args) {
|
|
@@ -1036,7 +1041,7 @@ function printHelp() {
|
|
|
1036
1041
|
console.log('Other');
|
|
1037
1042
|
console.log(' about');
|
|
1038
1043
|
console.log(' web [--out .plugscout/report.html] [--kind ...] [--limit n] [--open]');
|
|
1039
|
-
console.log(' client setup --client cursor|gemini [--scope user|project] [--force]');
|
|
1044
|
+
console.log(' client setup --client cursor|gemini|claude-desktop|windsurf|opencode|zed [--scope user|project] [--force]');
|
|
1040
1045
|
console.log(' upgrade check');
|
|
1041
1046
|
console.log(' help');
|
|
1042
1047
|
console.log('');
|
|
@@ -1057,6 +1062,10 @@ function printHelp() {
|
|
|
1057
1062
|
console.log(' plugscout show --id claude-connector:asana');
|
|
1058
1063
|
console.log(' plugscout client setup --client cursor');
|
|
1059
1064
|
console.log(' plugscout client setup --client gemini');
|
|
1065
|
+
console.log(' plugscout client setup --client claude-desktop');
|
|
1066
|
+
console.log(' plugscout client setup --client windsurf');
|
|
1067
|
+
console.log(' plugscout client setup --client opencode');
|
|
1068
|
+
console.log(' plugscout client setup --client zed');
|
|
1060
1069
|
console.log(' plugscout sync --kind cursor-extension,gemini-extension');
|
|
1061
1070
|
console.log('');
|
|
1062
1071
|
console.log('Global options');
|
package/package.json
CHANGED