@shnitzel/plugscout 0.3.26 → 0.3.28

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.
@@ -480,7 +480,7 @@ async function handleList(args) {
480
480
  if (details) {
481
481
  console.log(renderCatalogDecisionDetails(filtered, policy, insights));
482
482
  }
483
- await promptResultBrowser(filtered.map((e) => ({ id: e.item.id, name: e.item.name })));
483
+ await promptResultBrowser(filtered.map((e) => ({ id: e.item.id, name: e.item.name, url: buildItemLinks(e.item)[0]?.url })));
484
484
  }
485
485
  async function handleShow(args) {
486
486
  const id = readFlag(args, '--id');
@@ -560,7 +560,7 @@ async function handleSearch(args) {
560
560
  name: entry.item.name
561
561
  }))));
562
562
  printHint('Use `show --id <catalog-id>` for full detail.');
563
- await promptResultBrowser(matches.map((e) => ({ id: e.item.id, name: e.item.name })));
563
+ await promptResultBrowser(matches.map((e) => ({ id: e.item.id, name: e.item.name, url: buildItemLinks(e.item)[0]?.url })));
564
564
  }
565
565
  async function handleExplain(args) {
566
566
  const kinds = readKinds(args);
@@ -681,12 +681,16 @@ async function handleTop(args) {
681
681
  printHint('Review each suggestion before installing. Do not install blindly from rank alone.');
682
682
  printHint(`Risk scale (lower is safer): ${formatRiskScale(policy)}`);
683
683
  printHint('Use `show --id <catalog-id>` or `assess --id <catalog-id>` for deep inspection.');
684
+ const topCatalogItems = await loadCatalogItems();
685
+ const topCatalogMap = new Map(topCatalogItems.map((item) => [item.id, item]));
684
686
  if (details) {
685
- const [catalogItems, insights] = await Promise.all([loadCatalogItems(), loadItemInsights()]);
686
- const catalogMap = new Map(catalogItems.map((item) => [item.id, item]));
687
- console.log(renderRecommendationDecisionDetails(safe, catalogMap, policy, insights));
687
+ const insights = await loadItemInsights();
688
+ console.log(renderRecommendationDecisionDetails(safe, topCatalogMap, policy, insights));
688
689
  }
689
- await promptResultBrowser(safe.map((e) => ({ id: e.id, name: '' })));
690
+ await promptResultBrowser(safe.map((e) => {
691
+ const item = topCatalogMap.get(e.id);
692
+ return { id: e.id, name: '', url: item ? buildItemLinks(item)[0]?.url : undefined };
693
+ }));
690
694
  }
691
695
  async function handleSync(args) {
692
696
  const kinds = readKinds(args);
@@ -787,7 +791,12 @@ async function handleRecommend(args) {
787
791
  }
788
792
  printHint('Next: run `show --id <catalog-id>` or `install --id <catalog-id> --yes`.');
789
793
  if (format !== 'json') {
790
- await promptResultBrowser(ranked.map((e) => ({ id: e.id, name: '' })));
794
+ const recCatalogItems = await loadCatalogItems();
795
+ const recCatalogMap = new Map(recCatalogItems.map((item) => [item.id, item]));
796
+ await promptResultBrowser(ranked.map((e) => {
797
+ const item = recCatalogMap.get(e.id);
798
+ return { id: e.id, name: '', url: item ? buildItemLinks(item)[0]?.url : undefined };
799
+ }));
791
800
  }
792
801
  }
793
802
  async function handleWeb(args) {
@@ -1173,15 +1182,16 @@ async function promptResultBrowser(entries) {
1173
1182
  const ENTER = '\r';
1174
1183
  const CTRL_C = '';
1175
1184
  const Q = 'q';
1176
- // Single-line navigator no second table. The rendered results above are the reference.
1185
+ // Two-line navigator: id/name line + url line (blank if none). Always writes 2 lines so cursor math is stable.
1177
1186
  function renderNavigator(firstRender) {
1178
1187
  if (!firstRender) {
1179
- moveCursor(process.stdout, 0, -1);
1188
+ moveCursor(process.stdout, 0, -2);
1180
1189
  clearScreenDown(process.stdout);
1181
1190
  }
1182
1191
  const entry = entries[selected];
1183
1192
  const label = entry.name ? `${entry.id} \x1b[90m${entry.name}\x1b[0m` : entry.id;
1184
1193
  process.stdout.write(` \x1b[36m❯\x1b[0m ${label} \x1b[90m(${selected + 1}/${entries.length})\x1b[0m\n`);
1194
+ process.stdout.write(entry.url ? ` \x1b[90m${entry.url}\x1b[0m\n` : '\n');
1185
1195
  }
1186
1196
  // eslint-disable-next-line prefer-const
1187
1197
  let running = true;
@@ -107,6 +107,9 @@ async function checkNpmRegistry(pkg) {
107
107
  }
108
108
  async function checkVscodeMarketplace(vsixId) {
109
109
  const checkedAt = new Date().toISOString();
110
+ const error = (status, text) => ({
111
+ result: { source: 'vscode-marketplace', label: 'VS Code Marketplace', status, findings: text ? [{ severity: 'info', text }] : [], checkedAt },
112
+ });
110
113
  try {
111
114
  const res = await fetchWithTimeout('https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery', {
112
115
  method: 'POST',
@@ -117,21 +120,28 @@ async function checkVscodeMarketplace(vsixId) {
117
120
  },
118
121
  body: JSON.stringify({
119
122
  filters: [{ criteria: [{ filterType: 7, value: vsixId }] }],
120
- flags: 0x200 | 0x100 | 0x20, // statistics + assetUri + excludeNonValidated
123
+ // statistics (0x200) + assetUri (0x100) + excludeNonValidated (0x20) + versionProperties (0x10)
124
+ flags: 0x200 | 0x100 | 0x20 | 0x10,
121
125
  }),
122
126
  }, 8000);
123
- if (!res.ok) {
124
- return { source: 'vscode-marketplace', label: 'VS Code Marketplace', status: 'error', findings: [], checkedAt };
125
- }
127
+ if (!res.ok)
128
+ return error('error');
126
129
  const data = await res.json();
127
130
  const ext = data.results?.[0]?.extensions?.[0];
128
131
  if (!ext) {
129
132
  return {
130
- source: 'vscode-marketplace', label: 'VS Code Marketplace', status: 'unavailable',
131
- findings: [{ severity: 'warn', text: 'Extension not found in marketplace — may have been removed' }],
132
- checkedAt,
133
+ result: {
134
+ source: 'vscode-marketplace', label: 'VS Code Marketplace', status: 'unavailable',
135
+ findings: [{ severity: 'warn', text: 'Extension not found in marketplace — may have been removed' }],
136
+ checkedAt,
137
+ },
133
138
  };
134
139
  }
140
+ // Extract GitHub repository URL from version properties
141
+ const props = ext.versions?.[0]?.properties ?? [];
142
+ const repoProp = props.find(p => p.key === 'Microsoft.VisualStudio.Services.Links.Source' ||
143
+ p.key === 'Microsoft.VisualStudio.Services.Links.GitHub');
144
+ const repositoryUrl = repoProp?.value?.includes('github.com') ? repoProp.value : undefined;
135
145
  const findings = [];
136
146
  const verified = ext.publisher.isDomainVerified ?? false;
137
147
  const latest = ext.versions?.[0];
@@ -147,13 +157,16 @@ async function checkVscodeMarketplace(vsixId) {
147
157
  findings.push({ severity: 'info', text: `${installs.toLocaleString()} installs` });
148
158
  }
149
159
  return {
150
- source: 'vscode-marketplace', label: 'VS Code Marketplace',
151
- status: verified ? 'clean' : 'flagged',
152
- findings, checkedAt,
160
+ result: {
161
+ source: 'vscode-marketplace', label: 'VS Code Marketplace',
162
+ status: verified ? 'clean' : 'flagged',
163
+ findings, checkedAt,
164
+ },
165
+ repositoryUrl,
153
166
  };
154
167
  }
155
168
  catch {
156
- return { source: 'vscode-marketplace', label: 'VS Code Marketplace', status: 'error', findings: [{ severity: 'info', text: 'Check timed out' }], checkedAt };
169
+ return error('error', 'Check timed out');
157
170
  }
158
171
  }
159
172
  function extractGithubOwnerRepo(url) {
@@ -258,15 +271,24 @@ export async function runLiveChecks(item, opts = {}) {
258
271
  checks.push(checkOsv(npmPkg));
259
272
  checks.push(checkNpmRegistry(npmPkg));
260
273
  }
274
+ // cursor-extension: marketplace first (to extract repo URL), then github check if found
275
+ const cursorMarketplaceResults = [];
261
276
  if (item.kind === 'cursor-extension') {
262
277
  const vsixId = typeof meta.vsixId === 'string' ? meta.vsixId : null;
263
278
  if (vsixId) {
264
- checks.push(checkVscodeMarketplace(vsixId));
279
+ const mkt = await checkVscodeMarketplace(vsixId);
280
+ cursorMarketplaceResults.push(mkt.result);
281
+ const ghOwnerRepo = mkt.repositoryUrl ? extractGithubOwnerRepo(mkt.repositoryUrl) : findGithubOwnerRepo(item, meta);
282
+ if (ghOwnerRepo) {
283
+ checks.push(checkGithubRepo(ghOwnerRepo));
284
+ }
265
285
  }
266
286
  }
267
- const ghOwnerRepo = findGithubOwnerRepo(item, meta);
268
- if (ghOwnerRepo) {
269
- checks.push(checkGithubRepo(ghOwnerRepo));
287
+ else {
288
+ const ghOwnerRepo = findGithubOwnerRepo(item, meta);
289
+ if (ghOwnerRepo) {
290
+ checks.push(checkGithubRepo(ghOwnerRepo));
291
+ }
270
292
  }
271
293
  if (item.kind === 'claude-plugin' || item.kind === 'claude-connector') {
272
294
  const installUrl = item.install.url;
@@ -274,9 +296,9 @@ export async function runLiveChecks(item, opts = {}) {
274
296
  checks.push(checkUrlHealth(installUrl));
275
297
  }
276
298
  }
277
- if (checks.length === 0)
299
+ const results = [...cursorMarketplaceResults, ...(checks.length > 0 ? await Promise.all(checks) : [])];
300
+ if (results.length === 0)
278
301
  return [];
279
- const results = await Promise.all(checks);
280
302
  await writeCache(item.id, results);
281
303
  return results;
282
304
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shnitzel/plugscout",
3
- "version": "0.3.26",
3
+ "version": "0.3.28",
4
4
  "description": "Claude plugins + Claude connectors + Copilot extensions + Skills + MCP security intelligence framework",
5
5
  "private": false,
6
6
  "type": "module",