@shnitzel/plugscout 0.3.8 → 0.3.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/config/providers.json +20 -0
- package/config/registries.json +644 -0
- package/dist/catalog/adapter.js +8 -0
- package/dist/catalog/adapters/cursor-extensions-v1.js +60 -0
- package/dist/catalog/adapters/gemini-extensions-v1.js +64 -0
- package/dist/catalog/remote-registry.js +12 -1
- package/dist/catalog/sync.js +13 -3
- package/dist/interfaces/cli/client-setup.js +60 -0
- package/dist/interfaces/cli/doctor.js +30 -0
- package/dist/interfaces/cli/index.js +45 -6
- package/dist/interfaces/cli/mcp.js +1 -1
- package/dist/interfaces/cli/options.js +8 -2
- package/dist/interfaces/cli/ui/home.js +63 -49
- package/dist/interfaces/cli/ui/web-report.js +231 -164
- package/dist/lib/validation/contracts.js +4 -2
- package/package.json +1 -1
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { dedupe, extractStringArray, readString, toCount, toScore } from './shared.js';
|
|
2
|
+
export function adaptCursorExtensionsEntries(sourceId, entries) {
|
|
3
|
+
return entries
|
|
4
|
+
.map((entry) => mapCursorExtensionEntry(sourceId, entry))
|
|
5
|
+
.filter((entry) => entry !== null);
|
|
6
|
+
}
|
|
7
|
+
function mapCursorExtensionEntry(sourceId, entry) {
|
|
8
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
const record = entry;
|
|
12
|
+
const slug = readString(record, ['slug', 'id', 'name']);
|
|
13
|
+
if (!slug) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
const name = readString(record, ['title', 'name']) ?? slug;
|
|
17
|
+
const description = readString(record, ['description', 'summary']) ?? `Cursor extension ${name}`;
|
|
18
|
+
const capabilities = dedupe(extractStringArray(record, ['capabilities', 'tools']).concat(extractStringArray(record, ['tags'])));
|
|
19
|
+
const compatibility = dedupe(extractStringArray(record, ['compatibility']).concat(['cursor']));
|
|
20
|
+
const metadata = record.metadata && typeof record.metadata === 'object' && !Array.isArray(record.metadata)
|
|
21
|
+
? record.metadata
|
|
22
|
+
: {};
|
|
23
|
+
const vsixId = typeof record.vsixId === 'string' ? record.vsixId : typeof metadata.vsixId === 'string' ? metadata.vsixId : undefined;
|
|
24
|
+
const installInstructions = readString(record, ['install', 'instructions']) ??
|
|
25
|
+
(vsixId
|
|
26
|
+
? `Install via Cursor Marketplace: open Extensions panel (Cmd+Shift+X) and search for ${name}.`
|
|
27
|
+
: `Install via Cursor Marketplace: open Extensions panel (Cmd+Shift+X) and search for ${name}.`);
|
|
28
|
+
const installUrl = readString(record, ['install', 'url']) ??
|
|
29
|
+
(vsixId ? `https://marketplace.visualstudio.com/items?itemName=${vsixId}` : undefined);
|
|
30
|
+
return {
|
|
31
|
+
id: slug.startsWith('cursor-extension:') ? slug : `cursor-extension:${slug}`,
|
|
32
|
+
kind: 'cursor-extension',
|
|
33
|
+
provider: readString(record, ['provider']) ?? 'cursor',
|
|
34
|
+
name,
|
|
35
|
+
description,
|
|
36
|
+
capabilities,
|
|
37
|
+
compatibility,
|
|
38
|
+
source: sourceId,
|
|
39
|
+
install: {
|
|
40
|
+
kind: 'manual',
|
|
41
|
+
instructions: installInstructions,
|
|
42
|
+
...(installUrl ? { url: installUrl } : {})
|
|
43
|
+
},
|
|
44
|
+
adoptionSignal: toScore(record.adoptionSignal, 50),
|
|
45
|
+
maintenanceSignal: toScore(record.maintenanceSignal, 50),
|
|
46
|
+
provenanceSignal: toScore(record.provenanceSignal, 70),
|
|
47
|
+
freshnessSignal: toScore(record.freshnessSignal, 60),
|
|
48
|
+
securitySignals: {
|
|
49
|
+
knownVulnerabilities: toCount(record.knownVulnerabilities),
|
|
50
|
+
suspiciousPatterns: toCount(record.suspiciousPatterns),
|
|
51
|
+
injectionFindings: toCount(record.injectionFindings),
|
|
52
|
+
exfiltrationSignals: toCount(record.exfiltrationSignals),
|
|
53
|
+
integrityAlerts: toCount(record.integrityAlerts)
|
|
54
|
+
},
|
|
55
|
+
metadata: {
|
|
56
|
+
...metadata,
|
|
57
|
+
...(vsixId ? { vsixId } : {})
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { dedupe, extractStringArray, readString, toCount, toScore } from './shared.js';
|
|
2
|
+
export function adaptGeminiExtensionsEntries(sourceId, entries) {
|
|
3
|
+
return entries
|
|
4
|
+
.map((entry) => mapGeminiExtensionEntry(sourceId, entry))
|
|
5
|
+
.filter((entry) => entry !== null);
|
|
6
|
+
}
|
|
7
|
+
function mapGeminiExtensionEntry(sourceId, entry) {
|
|
8
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
const record = entry;
|
|
12
|
+
const slug = readString(record, ['slug', 'id', 'name']);
|
|
13
|
+
if (!slug) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
const name = readString(record, ['title', 'name']) ?? slug;
|
|
17
|
+
const description = readString(record, ['description', 'summary']) ?? `Gemini CLI extension ${name}`;
|
|
18
|
+
const capabilities = dedupe(extractStringArray(record, ['capabilities', 'tools']).concat(extractStringArray(record, ['tags'])));
|
|
19
|
+
const compatibility = dedupe(extractStringArray(record, ['compatibility']).concat(['gemini', 'gemini-cli']));
|
|
20
|
+
const metadata = record.metadata && typeof record.metadata === 'object' && !Array.isArray(record.metadata)
|
|
21
|
+
? record.metadata
|
|
22
|
+
: {};
|
|
23
|
+
const npmPkg = typeof record.npmPackage === 'string'
|
|
24
|
+
? record.npmPackage
|
|
25
|
+
: typeof metadata.npmPackage === 'string'
|
|
26
|
+
? metadata.npmPackage
|
|
27
|
+
: undefined;
|
|
28
|
+
const installInstructions = readString(record, ['install', 'instructions']) ??
|
|
29
|
+
(npmPkg
|
|
30
|
+
? `Add to ~/.gemini/settings.json under mcpServers: { "${slug.replace('gemini-extension:', '')}": { "command": "npx", "args": ["-y", "${npmPkg}"] } }`
|
|
31
|
+
: `Configure in ~/.gemini/settings.json under mcpServers.`);
|
|
32
|
+
const installUrl = readString(record, ['install', 'url']) ??
|
|
33
|
+
(npmPkg ? `https://www.npmjs.com/package/${npmPkg}` : undefined);
|
|
34
|
+
return {
|
|
35
|
+
id: slug.startsWith('gemini-extension:') ? slug : `gemini-extension:${slug}`,
|
|
36
|
+
kind: 'gemini-extension',
|
|
37
|
+
provider: readString(record, ['provider']) ?? 'google',
|
|
38
|
+
name,
|
|
39
|
+
description,
|
|
40
|
+
capabilities,
|
|
41
|
+
compatibility,
|
|
42
|
+
source: sourceId,
|
|
43
|
+
install: {
|
|
44
|
+
kind: 'manual',
|
|
45
|
+
instructions: installInstructions,
|
|
46
|
+
...(installUrl ? { url: installUrl } : {})
|
|
47
|
+
},
|
|
48
|
+
adoptionSignal: toScore(record.adoptionSignal, 50),
|
|
49
|
+
maintenanceSignal: toScore(record.maintenanceSignal, 50),
|
|
50
|
+
provenanceSignal: toScore(record.provenanceSignal, 75),
|
|
51
|
+
freshnessSignal: toScore(record.freshnessSignal, 60),
|
|
52
|
+
securitySignals: {
|
|
53
|
+
knownVulnerabilities: toCount(record.knownVulnerabilities),
|
|
54
|
+
suspiciousPatterns: toCount(record.suspiciousPatterns),
|
|
55
|
+
injectionFindings: toCount(record.injectionFindings),
|
|
56
|
+
exfiltrationSignals: toCount(record.exfiltrationSignals),
|
|
57
|
+
integrityAlerts: toCount(record.integrityAlerts)
|
|
58
|
+
},
|
|
59
|
+
metadata: {
|
|
60
|
+
...metadata,
|
|
61
|
+
...(npmPkg ? { npmPackage: npmPkg } : {})
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -132,6 +132,12 @@ function defaultCatalogKeyByKind(kind) {
|
|
|
132
132
|
if (kind === 'copilot-extension') {
|
|
133
133
|
return 'extensions';
|
|
134
134
|
}
|
|
135
|
+
if (kind === 'cursor-extension') {
|
|
136
|
+
return 'extensions';
|
|
137
|
+
}
|
|
138
|
+
if (kind === 'gemini-extension') {
|
|
139
|
+
return 'extensions';
|
|
140
|
+
}
|
|
135
141
|
return 'skills';
|
|
136
142
|
}
|
|
137
143
|
function resolveByPath(value, path) {
|
|
@@ -192,5 +198,10 @@ function validateRemoteHost(registry) {
|
|
|
192
198
|
}
|
|
193
199
|
}
|
|
194
200
|
function requiresSafeHostAllowlist(kind) {
|
|
195
|
-
return kind === 'claude-plugin' ||
|
|
201
|
+
return (kind === 'claude-plugin' ||
|
|
202
|
+
kind === 'claude-connector' ||
|
|
203
|
+
kind === 'copilot-extension' ||
|
|
204
|
+
kind === 'cursor-extension' ||
|
|
205
|
+
kind === 'gemini-extension' ||
|
|
206
|
+
kind === 'mcp');
|
|
196
207
|
}
|
package/dist/catalog/sync.js
CHANGED
|
@@ -38,7 +38,7 @@ export async function syncCatalogs(today = new Date().toISOString().slice(0, 10)
|
|
|
38
38
|
await Promise.all([saveCatalogItems(mergedItems), saveLegacyCatalogViews(mergedItems)]);
|
|
39
39
|
await saveSyncState(syncState);
|
|
40
40
|
const kindCounts = countByKind(mergedItems);
|
|
41
|
-
logger.info(`Synced ${mergedItems.length} items (${kindCounts.skill} skills, ${kindCounts.mcp} MCPs, ${kindCounts['claude-plugin']} Claude plugins, ${kindCounts['claude-connector']} Claude connectors, ${kindCounts['copilot-extension']} Copilot extensions)`);
|
|
41
|
+
logger.info(`Synced ${mergedItems.length} items (${kindCounts.skill} skills, ${kindCounts.mcp} MCPs, ${kindCounts['claude-plugin']} Claude plugins, ${kindCounts['claude-connector']} Claude connectors, ${kindCounts['copilot-extension']} Copilot extensions, ${kindCounts['cursor-extension']} Cursor extensions, ${kindCounts['gemini-extension']} Gemini extensions)`);
|
|
42
42
|
const staleRegistries = getStaleRegistries(syncState);
|
|
43
43
|
if (staleRegistries.length > 0) {
|
|
44
44
|
logger.warn(`Stale registries (>48h without success): ${staleRegistries.join(', ')}`);
|
|
@@ -84,7 +84,9 @@ function normalizeId(id, kind) {
|
|
|
84
84
|
mcp: 'mcp',
|
|
85
85
|
'claude-plugin': 'claude-plugin',
|
|
86
86
|
'claude-connector': 'claude-connector',
|
|
87
|
-
'copilot-extension': 'copilot-extension'
|
|
87
|
+
'copilot-extension': 'copilot-extension',
|
|
88
|
+
'cursor-extension': 'cursor-extension',
|
|
89
|
+
'gemini-extension': 'gemini-extension'
|
|
88
90
|
};
|
|
89
91
|
return `${prefixMap[kind]}:${id.replace(/^([a-z-]+):/, '')}`;
|
|
90
92
|
}
|
|
@@ -101,6 +103,12 @@ function inferProviderFromKind(kind) {
|
|
|
101
103
|
if (kind === 'copilot-extension') {
|
|
102
104
|
return 'github';
|
|
103
105
|
}
|
|
106
|
+
if (kind === 'cursor-extension') {
|
|
107
|
+
return 'cursor';
|
|
108
|
+
}
|
|
109
|
+
if (kind === 'gemini-extension') {
|
|
110
|
+
return 'google';
|
|
111
|
+
}
|
|
104
112
|
return 'openai';
|
|
105
113
|
}
|
|
106
114
|
function mergeItemsById(items) {
|
|
@@ -142,7 +150,9 @@ function countByKind(items) {
|
|
|
142
150
|
mcp: 0,
|
|
143
151
|
'claude-plugin': 0,
|
|
144
152
|
'claude-connector': 0,
|
|
145
|
-
'copilot-extension': 0
|
|
153
|
+
'copilot-extension': 0,
|
|
154
|
+
'cursor-extension': 0,
|
|
155
|
+
'gemini-extension': 0
|
|
146
156
|
});
|
|
147
157
|
}
|
|
148
158
|
function defaultSourceConfidence(sourceType) {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
const PLUGSCOUT_MCP_VALUE = { command: 'npx', args: ['plugscout', 'mcp'] };
|
|
5
|
+
export function getConfigPath(client, scope) {
|
|
6
|
+
if (client === 'cursor') {
|
|
7
|
+
if (scope === 'project') {
|
|
8
|
+
return path.join(process.cwd(), '.cursor', 'mcp.json');
|
|
9
|
+
}
|
|
10
|
+
return path.join(os.homedir(), '.cursor', 'mcp.json');
|
|
11
|
+
}
|
|
12
|
+
return path.join(os.homedir(), '.gemini', 'settings.json');
|
|
13
|
+
}
|
|
14
|
+
export async function getClientMcpConfigStatus(client, scope) {
|
|
15
|
+
const configPath = getConfigPath(client, scope);
|
|
16
|
+
try {
|
|
17
|
+
const raw = await fs.readFile(configPath, 'utf8');
|
|
18
|
+
const config = JSON.parse(raw);
|
|
19
|
+
const mcpServers = config.mcpServers;
|
|
20
|
+
const configured = !!(mcpServers?.plugscout);
|
|
21
|
+
return { configured, configPath };
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return { configured: false, configPath };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export async function writeClientMcpConfig(options) {
|
|
28
|
+
const { client, force = false } = options;
|
|
29
|
+
const scope = client === 'gemini' ? 'user' : options.scope;
|
|
30
|
+
const configPath = getConfigPath(client, scope);
|
|
31
|
+
let existing = {};
|
|
32
|
+
try {
|
|
33
|
+
const raw = await fs.readFile(configPath, 'utf8');
|
|
34
|
+
existing = JSON.parse(raw);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// file doesn't exist yet — start empty
|
|
38
|
+
}
|
|
39
|
+
const mcpServers = existing.mcpServers ?? {};
|
|
40
|
+
const current = mcpServers.plugscout;
|
|
41
|
+
if (current !== undefined) {
|
|
42
|
+
const isSame = JSON.stringify(current) === JSON.stringify(PLUGSCOUT_MCP_VALUE);
|
|
43
|
+
if (isSame) {
|
|
44
|
+
return { status: 'already-configured', configPath };
|
|
45
|
+
}
|
|
46
|
+
if (!force) {
|
|
47
|
+
throw new Error(`plugscout already exists in ${configPath} with a different value. Use --force to overwrite.`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const updated = {
|
|
51
|
+
...existing,
|
|
52
|
+
mcpServers: { ...mcpServers, plugscout: PLUGSCOUT_MCP_VALUE }
|
|
53
|
+
};
|
|
54
|
+
const dir = path.dirname(configPath);
|
|
55
|
+
await fs.mkdir(dir, { recursive: true });
|
|
56
|
+
const tmpPath = `${configPath}.plugscout.tmp`;
|
|
57
|
+
await fs.writeFile(tmpPath, `${JSON.stringify(updated, null, 2)}\n`, 'utf8');
|
|
58
|
+
await fs.rename(tmpPath, configPath);
|
|
59
|
+
return { status: 'written', configPath };
|
|
60
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
3
4
|
import fs from 'node:fs/promises';
|
|
4
5
|
import { getStaleRegistries, loadSyncState } from '../../catalog/sync-state.js';
|
|
5
6
|
import { loadCatalogItems } from '../../catalog/repository.js';
|
|
6
7
|
import { hasLegacySkillSh, resolveSkillsRuntime } from '../../install/dependencies.js';
|
|
8
|
+
import { getClientMcpConfigStatus } from './client-setup.js';
|
|
7
9
|
export async function runDoctorChecks(projectPath = '.') {
|
|
8
10
|
const checks = [];
|
|
9
11
|
checks.push(checkSkillsRuntime());
|
|
@@ -58,6 +60,34 @@ export async function runDoctorChecks(projectPath = '.') {
|
|
|
58
60
|
suggestion: 'Run: npm run dev -- init'
|
|
59
61
|
});
|
|
60
62
|
}
|
|
63
|
+
// Cursor IDE check
|
|
64
|
+
const cursorInstalled = spawnSync('which', ['cursor'], { encoding: 'utf8' }).status === 0 ||
|
|
65
|
+
await fs.access(path.join(os.homedir(), '.cursor')).then(() => true).catch(() => false);
|
|
66
|
+
checks.push(cursorInstalled
|
|
67
|
+
? { name: 'Cursor IDE', status: 'pass', message: 'Cursor detected' }
|
|
68
|
+
: { name: 'Cursor IDE', status: 'warn', message: 'Cursor not detected', suggestion: 'Install Cursor from https://cursor.sh' });
|
|
69
|
+
// Gemini CLI check
|
|
70
|
+
checks.push(checkBinary('gemini', { suggestion: 'Install Gemini CLI: npm install -g @google/gemini-cli' }));
|
|
71
|
+
// Cursor MCP config check
|
|
72
|
+
try {
|
|
73
|
+
const cursorStatus = await getClientMcpConfigStatus('cursor', 'user');
|
|
74
|
+
checks.push(cursorStatus.configured
|
|
75
|
+
? { name: 'Cursor MCP config', status: 'pass', message: `plugscout wired in ${cursorStatus.configPath}` }
|
|
76
|
+
: { name: 'Cursor MCP config', status: 'warn', message: 'plugscout not in Cursor MCP config', suggestion: 'Run: plugscout client setup --client cursor' });
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
checks.push({ name: 'Cursor MCP config', status: 'warn', message: 'Could not read Cursor MCP config', suggestion: 'Run: plugscout client setup --client cursor' });
|
|
80
|
+
}
|
|
81
|
+
// Gemini MCP config check
|
|
82
|
+
try {
|
|
83
|
+
const geminiStatus = await getClientMcpConfigStatus('gemini', 'user');
|
|
84
|
+
checks.push(geminiStatus.configured
|
|
85
|
+
? { name: 'Gemini MCP config', status: 'pass', message: `plugscout wired in ${geminiStatus.configPath}` }
|
|
86
|
+
: { name: 'Gemini MCP config', status: 'warn', message: 'plugscout not in Gemini MCP config', suggestion: 'Run: plugscout client setup --client gemini' });
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
checks.push({ name: 'Gemini MCP config', status: 'warn', message: 'Could not read Gemini MCP config', suggestion: 'Run: plugscout client setup --client gemini' });
|
|
90
|
+
}
|
|
61
91
|
return checks;
|
|
62
92
|
}
|
|
63
93
|
function checkSkillsRuntime() {
|
|
@@ -62,6 +62,7 @@ const COMMAND_ALIASES = {
|
|
|
62
62
|
setup: 'setup',
|
|
63
63
|
help: 'help',
|
|
64
64
|
mcp: 'mcp',
|
|
65
|
+
client: 'client',
|
|
65
66
|
};
|
|
66
67
|
export async function runCli(argv) {
|
|
67
68
|
const noUpdateCheck = hasFlag(argv, '--no-update-check');
|
|
@@ -136,6 +137,9 @@ export async function runCli(argv) {
|
|
|
136
137
|
case 'mcp':
|
|
137
138
|
await handleMcp(rest);
|
|
138
139
|
return; // intentional: MCP server is long-lived; update banner would corrupt JSON-RPC stream
|
|
140
|
+
case 'client':
|
|
141
|
+
await handleClient(rest);
|
|
142
|
+
break;
|
|
139
143
|
case 'help':
|
|
140
144
|
printHelp();
|
|
141
145
|
break;
|
|
@@ -166,7 +170,7 @@ async function handleAbout() {
|
|
|
166
170
|
if (pkg.author) {
|
|
167
171
|
console.log(`Author: ${pkg.author}`);
|
|
168
172
|
}
|
|
169
|
-
console.log('Scope: Claude plugins, Claude connectors, Copilot extensions, Skills, MCP servers');
|
|
173
|
+
console.log('Scope: Claude plugins, Claude connectors, Copilot extensions, Cursor extensions, Gemini extensions, Skills, MCP servers');
|
|
170
174
|
console.log('Ranking: trust-first (fit + trust - risk penalties + freshness bonus)');
|
|
171
175
|
console.log('Meaning: top/recommend output is repo-aware guidance, not a global popularity leaderboard.');
|
|
172
176
|
console.log('Install discipline: review each suggestion, check provenance and risk, and do not install blindly from rank alone.');
|
|
@@ -190,7 +194,7 @@ async function handleStatus(args) {
|
|
|
190
194
|
});
|
|
191
195
|
console.log('Catalog Status');
|
|
192
196
|
console.log(`Items: ${items.length}`);
|
|
193
|
-
console.log(`Kinds: skill=${kindCounts.get('skill') ?? 0}, mcp=${kindCounts.get('mcp') ?? 0}, claude-plugin=${kindCounts.get('claude-plugin') ?? 0}, claude-connector=${kindCounts.get('claude-connector') ?? 0}, copilot-extension=${kindCounts.get('copilot-extension') ?? 0}`);
|
|
197
|
+
console.log(`Kinds: skill=${kindCounts.get('skill') ?? 0}, mcp=${kindCounts.get('mcp') ?? 0}, claude-plugin=${kindCounts.get('claude-plugin') ?? 0}, claude-connector=${kindCounts.get('claude-connector') ?? 0}, copilot-extension=${kindCounts.get('copilot-extension') ?? 0}, cursor-extension=${kindCounts.get('cursor-extension') ?? 0}, gemini-extension=${kindCounts.get('gemini-extension') ?? 0}`);
|
|
194
198
|
console.log(`Providers: ${Array.from(providerCounts.entries())
|
|
195
199
|
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
196
200
|
.map(([name, count]) => `${name}=${count}`)
|
|
@@ -228,7 +232,7 @@ async function handleInit(args) {
|
|
|
228
232
|
const root = path.resolve(project);
|
|
229
233
|
const [items, policy] = await Promise.all([loadCatalogItems(), loadSecurityPolicy()]);
|
|
230
234
|
const providers = Array.from(new Set(items.map((item) => item.provider))).sort((a, b) => a.localeCompare(b));
|
|
231
|
-
const defaultKinds = ['skill', 'mcp', 'claude-plugin', 'claude-connector', 'copilot-extension'];
|
|
235
|
+
const defaultKinds = ['skill', 'mcp', 'claude-plugin', 'claude-connector', 'copilot-extension', 'cursor-extension', 'gemini-extension'];
|
|
232
236
|
const defaults = {
|
|
233
237
|
defaultKinds,
|
|
234
238
|
defaultProviders: providers,
|
|
@@ -298,7 +302,7 @@ async function handleSetup(args) {
|
|
|
298
302
|
console.log('Step 2/3: Initializing local config...');
|
|
299
303
|
const [items, policy] = await Promise.all([loadCatalogItems(), loadSecurityPolicy()]);
|
|
300
304
|
const providers = Array.from(new Set(items.map((item) => item.provider))).sort((a, b) => a.localeCompare(b));
|
|
301
|
-
const defaultKinds = ['skill', 'mcp', 'claude-plugin', 'claude-connector', 'copilot-extension'];
|
|
305
|
+
const defaultKinds = ['skill', 'mcp', 'claude-plugin', 'claude-connector', 'copilot-extension', 'cursor-extension', 'gemini-extension'];
|
|
302
306
|
const defaults = {
|
|
303
307
|
defaultKinds,
|
|
304
308
|
defaultProviders: providers,
|
|
@@ -853,6 +857,33 @@ async function handleQuarantine(args) {
|
|
|
853
857
|
const result = await applyQuarantineFromReport(report);
|
|
854
858
|
console.log(renderJson(result));
|
|
855
859
|
}
|
|
860
|
+
async function handleClient(args) {
|
|
861
|
+
const subcommand = args[0];
|
|
862
|
+
if (subcommand !== 'setup') {
|
|
863
|
+
throw new Error('Usage: client setup --client cursor|gemini [--scope user|project] [--force]');
|
|
864
|
+
}
|
|
865
|
+
const clientFlag = readFlag(args, '--client');
|
|
866
|
+
if (clientFlag !== 'cursor' && clientFlag !== 'gemini') {
|
|
867
|
+
throw new Error('Usage: client setup --client cursor|gemini');
|
|
868
|
+
}
|
|
869
|
+
const scopeFlag = readFlag(args, '--scope') ?? 'user';
|
|
870
|
+
if (scopeFlag !== 'user' && scopeFlag !== 'project') {
|
|
871
|
+
throw new Error('--scope must be user or project');
|
|
872
|
+
}
|
|
873
|
+
if (clientFlag === 'gemini' && scopeFlag === 'project') {
|
|
874
|
+
logger.warn('Gemini CLI only supports user scope; falling back to user scope.');
|
|
875
|
+
}
|
|
876
|
+
const force = hasFlag(args, '--force');
|
|
877
|
+
const { writeClientMcpConfig } = await import('./client-setup.js');
|
|
878
|
+
const result = await writeClientMcpConfig({ client: clientFlag, scope: scopeFlag, force });
|
|
879
|
+
if (result.status === 'already-configured') {
|
|
880
|
+
console.log(`plugscout already configured in ${result.configPath}`);
|
|
881
|
+
}
|
|
882
|
+
else {
|
|
883
|
+
console.log(`plugscout MCP config written: ${result.configPath}`);
|
|
884
|
+
printHint(`Restart ${clientFlag === 'cursor' ? 'Cursor IDE' : 'Gemini CLI'} for the change to take effect.`);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
856
887
|
async function handleUpgrade(args) {
|
|
857
888
|
const subcommand = args[0] ?? 'check';
|
|
858
889
|
if (subcommand !== 'check') {
|
|
@@ -980,7 +1011,7 @@ function printHelp() {
|
|
|
980
1011
|
console.log(' init [--project .]');
|
|
981
1012
|
console.log(' doctor [--project .] [--install-deps]');
|
|
982
1013
|
console.log(' status [--verbose]');
|
|
983
|
-
console.log(' sync [--kind skill,mcp,claude-plugin,claude-connector,copilot-extension] [--dry-run]');
|
|
1014
|
+
console.log(' sync [--kind skill,mcp,claude-plugin,claude-connector,copilot-extension,cursor-extension,gemini-extension] [--dry-run]');
|
|
984
1015
|
console.log('');
|
|
985
1016
|
console.log('Explore');
|
|
986
1017
|
console.log(' list [--kind ...] [--provider ...] [--risk-tier low|medium|high|critical] [--blocked true|false] [--search q] [--limit n] [--sort name|risk|trust] [--format json|table] [--readable] [--details]');
|
|
@@ -1005,6 +1036,7 @@ function printHelp() {
|
|
|
1005
1036
|
console.log('Other');
|
|
1006
1037
|
console.log(' about');
|
|
1007
1038
|
console.log(' web [--out .plugscout/report.html] [--kind ...] [--limit n] [--open]');
|
|
1039
|
+
console.log(' client setup --client cursor|gemini [--scope user|project] [--force]');
|
|
1008
1040
|
console.log(' upgrade check');
|
|
1009
1041
|
console.log(' help');
|
|
1010
1042
|
console.log('');
|
|
@@ -1014,11 +1046,18 @@ function printHelp() {
|
|
|
1014
1046
|
console.log(' plugins -> claude-plugin');
|
|
1015
1047
|
console.log(' connectors -> claude-connector');
|
|
1016
1048
|
console.log(' extensions, copilot -> copilot-extension');
|
|
1049
|
+
console.log(' cursor, cursor-extensions -> cursor-extension');
|
|
1050
|
+
console.log(' gemini, gemini-extensions -> gemini-extension');
|
|
1017
1051
|
console.log('');
|
|
1018
1052
|
console.log('Examples');
|
|
1019
1053
|
console.log(' plugscout recommend --project . --only-safe --limit 10');
|
|
1054
|
+
console.log(' plugscout list --kind cursor --limit 15');
|
|
1055
|
+
console.log(' plugscout list --kind gemini --limit 11');
|
|
1020
1056
|
console.log(' plugscout list --kind connectors --limit 10');
|
|
1021
1057
|
console.log(' plugscout show --id claude-connector:asana');
|
|
1058
|
+
console.log(' plugscout client setup --client cursor');
|
|
1059
|
+
console.log(' plugscout client setup --client gemini');
|
|
1060
|
+
console.log(' plugscout sync --kind cursor-extension,gemini-extension');
|
|
1022
1061
|
console.log('');
|
|
1023
1062
|
console.log('Global options');
|
|
1024
1063
|
console.log(' --no-update-check');
|
|
@@ -1032,7 +1071,7 @@ async function loadLocalCliConfig(projectRoot) {
|
|
|
1032
1071
|
return null;
|
|
1033
1072
|
}
|
|
1034
1073
|
return {
|
|
1035
|
-
defaultKinds: Array.isArray(parsed.defaultKinds) ? parsed.defaultKinds : ['skill', 'mcp', 'claude-plugin', 'claude-connector', 'copilot-extension'],
|
|
1074
|
+
defaultKinds: Array.isArray(parsed.defaultKinds) ? parsed.defaultKinds : ['skill', 'mcp', 'claude-plugin', 'claude-connector', 'copilot-extension', 'cursor-extension', 'gemini-extension'],
|
|
1036
1075
|
defaultProviders: Array.isArray(parsed.defaultProviders) ? parsed.defaultProviders : [],
|
|
1037
1076
|
riskPosture: parsed.riskPosture,
|
|
1038
1077
|
outputStyle: parsed.outputStyle === 'json' ? 'json' : 'rich-table',
|
|
@@ -18,7 +18,7 @@ export function createMcpServer(version = '0.0.0') {
|
|
|
18
18
|
type: 'object',
|
|
19
19
|
properties: {
|
|
20
20
|
query: { type: 'string', description: 'Search term' },
|
|
21
|
-
kind: { type: 'string', description: 'Filter by kind: skill, mcp, claude-plugin, claude-connector, copilot-extension' },
|
|
21
|
+
kind: { type: 'string', description: 'Filter by kind: skill, mcp, claude-plugin, claude-connector, copilot-extension, cursor-extension, gemini-extension' },
|
|
22
22
|
provider: { type: 'string', description: 'Filter by provider' },
|
|
23
23
|
limit: { type: 'number', description: 'Max results (default: 20)' },
|
|
24
24
|
},
|
|
@@ -24,7 +24,13 @@ const KIND_ALIASES = {
|
|
|
24
24
|
'copilot extensions': 'copilot-extension',
|
|
25
25
|
extension: 'copilot-extension',
|
|
26
26
|
extensions: 'copilot-extension',
|
|
27
|
-
copilot: 'copilot-extension'
|
|
27
|
+
copilot: 'copilot-extension',
|
|
28
|
+
'cursor-extension': 'cursor-extension',
|
|
29
|
+
'cursor-extensions': 'cursor-extension',
|
|
30
|
+
cursor: 'cursor-extension',
|
|
31
|
+
'gemini-extension': 'gemini-extension',
|
|
32
|
+
'gemini-extensions': 'gemini-extension',
|
|
33
|
+
gemini: 'gemini-extension'
|
|
28
34
|
};
|
|
29
35
|
export function readFlag(args, flag) {
|
|
30
36
|
const index = args.indexOf(flag);
|
|
@@ -57,7 +63,7 @@ export function normalizeKind(raw) {
|
|
|
57
63
|
return CatalogKindSchema.parse(normalized);
|
|
58
64
|
}
|
|
59
65
|
catch {
|
|
60
|
-
throw new Error(`Invalid --kind value: ${raw}. Expected one of: skill, mcp, claude-plugin, claude-connector, copilot-extension. Aliases also supported: skills, mcps, plugins, connectors, extensions.`);
|
|
66
|
+
throw new Error(`Invalid --kind value: ${raw}. Expected one of: skill, mcp, claude-plugin, claude-connector, copilot-extension, cursor-extension, gemini-extension. Aliases also supported: skills, mcps, plugins, connectors, extensions, cursor, gemini.`);
|
|
61
67
|
}
|
|
62
68
|
}
|
|
63
69
|
export function readCsvList(args, flag) {
|
|
@@ -207,60 +207,74 @@ export async function renderInteractiveHome() {
|
|
|
207
207
|
}
|
|
208
208
|
}
|
|
209
209
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
else if (key === ARROW_UP) {
|
|
226
|
-
selected = (selected - 1 + menuItems.length) % menuItems.length;
|
|
227
|
-
render(false);
|
|
228
|
-
}
|
|
229
|
-
else if (key === ARROW_DOWN) {
|
|
230
|
-
selected = (selected + 1) % menuItems.length;
|
|
231
|
-
render(false);
|
|
232
|
-
}
|
|
233
|
-
else if (key === ENTER) {
|
|
234
|
-
process.stdin.removeListener('data', onKey);
|
|
235
|
-
process.stdin.setRawMode(false);
|
|
236
|
-
process.stdin.pause();
|
|
237
|
-
process.stdout.write('\n');
|
|
238
|
-
const item = menuItems[selected];
|
|
239
|
-
if (!item.command) {
|
|
240
|
-
resolve();
|
|
210
|
+
let running = true;
|
|
211
|
+
while (running) {
|
|
212
|
+
process.stdout.write('\n');
|
|
213
|
+
process.stdin.setRawMode(true);
|
|
214
|
+
process.stdin.resume();
|
|
215
|
+
process.stdin.setEncoding('utf8');
|
|
216
|
+
render(true);
|
|
217
|
+
const action = await new Promise((resolve) => {
|
|
218
|
+
process.stdin.on('data', async function onKey(key) {
|
|
219
|
+
if (key === CTRL_C) {
|
|
220
|
+
process.stdin.removeListener('data', onKey);
|
|
221
|
+
process.stdin.setRawMode(false);
|
|
222
|
+
process.stdin.pause();
|
|
223
|
+
process.stdout.write('\n');
|
|
224
|
+
resolve({ exit: true });
|
|
241
225
|
return;
|
|
242
226
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
227
|
+
else if (key === ARROW_UP) {
|
|
228
|
+
selected = (selected - 1 + menuItems.length) % menuItems.length;
|
|
229
|
+
render(false);
|
|
230
|
+
}
|
|
231
|
+
else if (key === ARROW_DOWN) {
|
|
232
|
+
selected = (selected + 1) % menuItems.length;
|
|
233
|
+
render(false);
|
|
234
|
+
}
|
|
235
|
+
else if (key === ENTER) {
|
|
236
|
+
process.stdin.removeListener('data', onKey);
|
|
237
|
+
process.stdin.setRawMode(false);
|
|
238
|
+
process.stdin.pause();
|
|
239
|
+
process.stdout.write('\n');
|
|
240
|
+
const item = menuItems[selected];
|
|
241
|
+
if (!item.command) {
|
|
242
|
+
resolve({ exit: true });
|
|
255
243
|
return;
|
|
256
244
|
}
|
|
257
|
-
args = [...
|
|
245
|
+
let args = [...item.command];
|
|
246
|
+
if (item.needsId) {
|
|
247
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
248
|
+
process.stdin.resume();
|
|
249
|
+
const id = await new Promise((res) => {
|
|
250
|
+
rl.question(' Enter catalog ID: ', (answer) => {
|
|
251
|
+
rl.close();
|
|
252
|
+
res(answer.trim());
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
if (!id) {
|
|
256
|
+
resolve({ exit: false });
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
args = [...args, '--id', id];
|
|
260
|
+
}
|
|
261
|
+
resolve({ exit: false, args });
|
|
258
262
|
}
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
if (action.exit) {
|
|
266
|
+
running = false;
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
if (action.args) {
|
|
259
270
|
const cliPath = getPackagePath('dist/cli.js');
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
271
|
+
await new Promise((done) => {
|
|
272
|
+
const child = spawn(process.execPath, [cliPath, ...action.args], { stdio: 'inherit' });
|
|
273
|
+
child.on('close', () => done());
|
|
274
|
+
child.on('error', () => done());
|
|
275
|
+
});
|
|
263
276
|
}
|
|
264
|
-
|
|
265
|
-
|
|
277
|
+
process.stdout.write('\n');
|
|
278
|
+
}
|
|
279
|
+
}
|
|
266
280
|
}
|