@shnitzel/plugscout 0.3.23 → 0.3.25
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.
|
@@ -515,14 +515,12 @@ async function handleShow(args) {
|
|
|
515
515
|
printHint(`Install with: plugscout install --id ${item.id} --yes`);
|
|
516
516
|
printHint('Review provenance, risk, and capabilities first. Do not install blindly from a suggestion or score.');
|
|
517
517
|
console.log(`Provenance: source=${item.source} catalogType=${getCatalogType(item)} confidence=${getSourceConfidence(item)}`);
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
if (sourcePage) {
|
|
525
|
-
console.log(`Source page: ${sourcePage}`);
|
|
518
|
+
const links = buildItemLinks(item);
|
|
519
|
+
if (links.length > 0) {
|
|
520
|
+
console.log('\nLinks:');
|
|
521
|
+
for (const link of links) {
|
|
522
|
+
console.log(` ${link.label}: ${link.url}`);
|
|
523
|
+
}
|
|
526
524
|
}
|
|
527
525
|
}
|
|
528
526
|
async function handleSearch(args) {
|
|
@@ -816,6 +814,13 @@ async function handleAssess(args) {
|
|
|
816
814
|
console.log(renderJson(assessment));
|
|
817
815
|
await recordItemReview(found.id, 'assess');
|
|
818
816
|
printHint(`Risk scale (lower is safer): ${formatRiskScale(policy)}`);
|
|
817
|
+
const links = buildItemLinks(found);
|
|
818
|
+
if (links.length > 0) {
|
|
819
|
+
console.log('\nLinks:');
|
|
820
|
+
for (const link of links) {
|
|
821
|
+
console.log(` ${link.label}: ${link.url}`);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
819
824
|
}
|
|
820
825
|
async function handleInstall(args) {
|
|
821
826
|
const id = readFlag(args, '--id');
|
|
@@ -1143,52 +1148,35 @@ function describeRiskPosture(posture) {
|
|
|
1143
1148
|
async function promptResultBrowser(entries) {
|
|
1144
1149
|
if (!process.stdout.isTTY || entries.length === 0)
|
|
1145
1150
|
return;
|
|
1146
|
-
const WINDOW = 8;
|
|
1147
1151
|
let selected = 0;
|
|
1148
|
-
let linesDrawn = 0;
|
|
1149
1152
|
const ENTER = '\r';
|
|
1150
1153
|
const CTRL_C = '';
|
|
1151
1154
|
const Q = 'q';
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
}
|
|
1155
|
-
function renderBrowser(firstRender) {
|
|
1155
|
+
// Single-line navigator — no second table. The rendered results above are the reference.
|
|
1156
|
+
function renderNavigator(firstRender) {
|
|
1156
1157
|
if (!firstRender) {
|
|
1157
|
-
moveCursor(process.stdout, 0, -
|
|
1158
|
+
moveCursor(process.stdout, 0, -1);
|
|
1158
1159
|
clearScreenDown(process.stdout);
|
|
1159
1160
|
}
|
|
1160
|
-
|
|
1161
|
-
const
|
|
1162
|
-
|
|
1163
|
-
for (let i = start; i < end; i++) {
|
|
1164
|
-
const prefix = i === selected ? ' ❯ ' : ' ';
|
|
1165
|
-
const nameSuffix = entries[i].name ? ` ${entries[i].name}` : '';
|
|
1166
|
-
const line = `${prefix}${entries[i].id}${nameSuffix}`;
|
|
1167
|
-
process.stdout.write(`${line}\n`);
|
|
1168
|
-
drawn += 1;
|
|
1169
|
-
}
|
|
1170
|
-
if (entries.length > WINDOW) {
|
|
1171
|
-
process.stdout.write(`\x1b[2m (${selected + 1}/${entries.length})\x1b[0m\n`);
|
|
1172
|
-
drawn += 1;
|
|
1173
|
-
}
|
|
1174
|
-
linesDrawn = drawn;
|
|
1161
|
+
const entry = entries[selected];
|
|
1162
|
+
const label = entry.name ? `${entry.id} \x1b[90m${entry.name}\x1b[0m` : entry.id;
|
|
1163
|
+
process.stdout.write(` \x1b[36m❯\x1b[0m ${label} \x1b[90m(${selected + 1}/${entries.length})\x1b[0m\n`);
|
|
1175
1164
|
}
|
|
1176
1165
|
// eslint-disable-next-line prefer-const
|
|
1177
1166
|
let running = true;
|
|
1178
1167
|
let firstLoop = true;
|
|
1179
1168
|
while (running) {
|
|
1180
1169
|
if (firstLoop) {
|
|
1181
|
-
process.stdout.write('\x1b[
|
|
1170
|
+
process.stdout.write('\x1b[90m ↑↓ navigate ⏎ inspect q/Esc skip\x1b[0m\n');
|
|
1182
1171
|
firstLoop = false;
|
|
1183
1172
|
}
|
|
1184
1173
|
else {
|
|
1185
|
-
process.stdout.write('\
|
|
1174
|
+
process.stdout.write('\x1b[90m ↑↓ navigate ⏎ inspect another q/Esc done\x1b[0m\n');
|
|
1186
1175
|
}
|
|
1187
|
-
linesDrawn = 0;
|
|
1188
1176
|
process.stdin.setRawMode(true);
|
|
1189
1177
|
process.stdin.resume();
|
|
1190
1178
|
process.stdin.setEncoding('utf8');
|
|
1191
|
-
|
|
1179
|
+
renderNavigator(true);
|
|
1192
1180
|
const action = await new Promise((resolve) => {
|
|
1193
1181
|
let escTimer = null;
|
|
1194
1182
|
let pendingEsc = false;
|
|
@@ -1213,12 +1201,12 @@ async function promptResultBrowser(entries) {
|
|
|
1213
1201
|
}
|
|
1214
1202
|
if (key === '[A') {
|
|
1215
1203
|
selected = (selected - 1 + entries.length) % entries.length;
|
|
1216
|
-
|
|
1204
|
+
renderNavigator(false);
|
|
1217
1205
|
return;
|
|
1218
1206
|
}
|
|
1219
1207
|
if (key === '[B') {
|
|
1220
1208
|
selected = (selected + 1) % entries.length;
|
|
1221
|
-
|
|
1209
|
+
renderNavigator(false);
|
|
1222
1210
|
return;
|
|
1223
1211
|
}
|
|
1224
1212
|
// Standalone Esc followed by something unexpected — still exit
|
|
@@ -1231,12 +1219,12 @@ async function promptResultBrowser(entries) {
|
|
|
1231
1219
|
else if (key === '\x1b[A' || key === '\x1bOA') {
|
|
1232
1220
|
// Single-chunk arrow up (most terminals)
|
|
1233
1221
|
selected = (selected - 1 + entries.length) % entries.length;
|
|
1234
|
-
|
|
1222
|
+
renderNavigator(false);
|
|
1235
1223
|
}
|
|
1236
1224
|
else if (key === '\x1b[B' || key === '\x1bOB') {
|
|
1237
1225
|
// Single-chunk arrow down (most terminals)
|
|
1238
1226
|
selected = (selected + 1) % entries.length;
|
|
1239
|
-
|
|
1227
|
+
renderNavigator(false);
|
|
1240
1228
|
}
|
|
1241
1229
|
else if (key === '\x1b') {
|
|
1242
1230
|
// Could be standalone Esc or first byte of split arrow sequence
|
|
@@ -1477,3 +1465,43 @@ function getSourceConfidence(item) {
|
|
|
1477
1465
|
}
|
|
1478
1466
|
return 'official';
|
|
1479
1467
|
}
|
|
1468
|
+
function buildItemLinks(item) {
|
|
1469
|
+
const meta = getItemMetadata(item);
|
|
1470
|
+
const links = [];
|
|
1471
|
+
const seen = new Set();
|
|
1472
|
+
function add(label, url) {
|
|
1473
|
+
if (typeof url !== 'string' || !url.startsWith('http'))
|
|
1474
|
+
return;
|
|
1475
|
+
if (seen.has(url))
|
|
1476
|
+
return;
|
|
1477
|
+
seen.add(url);
|
|
1478
|
+
links.push({ label, url });
|
|
1479
|
+
}
|
|
1480
|
+
// Kind-specific derived URLs first (most useful)
|
|
1481
|
+
if (item.kind === 'cursor-extension') {
|
|
1482
|
+
const vsixId = meta.vsixId;
|
|
1483
|
+
if (typeof vsixId === 'string') {
|
|
1484
|
+
add('Marketplace', `https://marketplace.visualstudio.com/items?itemName=${vsixId}`);
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
if (item.kind === 'gemini-extension') {
|
|
1488
|
+
const pkg = meta.npmPackage;
|
|
1489
|
+
if (typeof pkg === 'string') {
|
|
1490
|
+
add('npm', `https://www.npmjs.com/package/${encodeURIComponent(pkg)}`);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
// install.url is the most direct link for the user
|
|
1494
|
+
add('Install page', item.install.url);
|
|
1495
|
+
// Metadata links — priority order
|
|
1496
|
+
add('Repository', meta.repositoryUrl);
|
|
1497
|
+
add('Repository', meta.githubUrl);
|
|
1498
|
+
add('Repository', meta.sourceRepo?.toString().startsWith('http') ? meta.sourceRepo : undefined);
|
|
1499
|
+
add('Website', meta.websiteUrl);
|
|
1500
|
+
add('Source page', meta.sourcePage);
|
|
1501
|
+
// GitHub shorthand (e.g. "github/awesome-copilot") → full URL
|
|
1502
|
+
if (typeof meta.sourceRepo === 'string' && !meta.sourceRepo.startsWith('http')) {
|
|
1503
|
+
const ghUrl = `https://github.com/${meta.sourceRepo}`;
|
|
1504
|
+
add('Repository', ghUrl);
|
|
1505
|
+
}
|
|
1506
|
+
return links;
|
|
1507
|
+
}
|
|
@@ -39,7 +39,7 @@ export async function renderHomeScreen() {
|
|
|
39
39
|
'plugscout sync --dry-run',
|
|
40
40
|
'plugscout help',
|
|
41
41
|
]) {
|
|
42
|
-
lines.push(` ${colorIfTty(cmd, colors.
|
|
42
|
+
lines.push(` ${colorIfTty(cmd, colors.cyan)}`);
|
|
43
43
|
}
|
|
44
44
|
lines.push('');
|
|
45
45
|
lines.push(colorIfTty('Examples', colors.bold));
|
|
@@ -49,7 +49,7 @@ export async function renderHomeScreen() {
|
|
|
49
49
|
'plugscout search github',
|
|
50
50
|
'plugscout show --id claude-connector:asana',
|
|
51
51
|
]) {
|
|
52
|
-
lines.push(` ${colorIfTty(cmd, colors.
|
|
52
|
+
lines.push(` ${colorIfTty(cmd, colors.cyan)}`);
|
|
53
53
|
}
|
|
54
54
|
lines.push('');
|
|
55
55
|
lines.push(colorIfTty('Kind aliases', colors.bold));
|
|
@@ -279,7 +279,7 @@ export async function renderInteractiveHome() {
|
|
|
279
279
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
280
280
|
process.stdin.resume();
|
|
281
281
|
const id = await new Promise((res) => {
|
|
282
|
-
rl.question('
|
|
282
|
+
rl.question(' Catalog ID (e.g. mcp:github, skill:code-review, cursor-extension:gitlens): ', (answer) => {
|
|
283
283
|
rl.close();
|
|
284
284
|
res(answer.trim());
|
|
285
285
|
});
|
|
@@ -454,10 +454,7 @@ function renderHtml(rows, allItems, stats, policy) {
|
|
|
454
454
|
</html>`;
|
|
455
455
|
}
|
|
456
456
|
function renderDetailCard(entry) {
|
|
457
|
-
const metadata = asMetadata(entry.item.metadata);
|
|
458
457
|
const trustScore = computeTrustScore(entry.item);
|
|
459
|
-
const sourceRepo = typeof metadata.sourceRepo === 'string' ? metadata.sourceRepo : '';
|
|
460
|
-
const sourcePage = typeof metadata.sourcePage === 'string' ? metadata.sourcePage : '';
|
|
461
458
|
const status = entry.blocked ? 'blocked' : entry.approved ? 'approved' : 'allowed';
|
|
462
459
|
const statusClass = entry.blocked ? 'bad' : 'ok';
|
|
463
460
|
const installHint = buildInstallHint(entry.item);
|
|
@@ -473,8 +470,6 @@ function renderDetailCard(entry) {
|
|
|
473
470
|
const bodyId = `body-${safeId}`;
|
|
474
471
|
const searchKey = escapeHtml(`${entry.item.id} ${entry.item.name} ${entry.item.capabilities.join(' ')}`.toLowerCase());
|
|
475
472
|
const plugscoutCmd = `plugscout install --id ${entry.item.id} --yes`;
|
|
476
|
-
const isManualUrl = entry.item.install.kind === 'manual' && typeof entry.item.install.url === 'string' && String(entry.item.install.url).startsWith('http');
|
|
477
|
-
const manualUrl = isManualUrl ? String(entry.item.install.url) : '';
|
|
478
473
|
const previewChips = entry.item.capabilities
|
|
479
474
|
.slice(0, 3)
|
|
480
475
|
.map((cap) => `<span class="chip">${escapeHtml(cap)}</span>`)
|
|
@@ -517,9 +512,7 @@ function renderDetailCard(entry) {
|
|
|
517
512
|
</div>
|
|
518
513
|
${entry.assessment.reasons.length > 0 ? `<div class="line"><span class="label">Risk signals: </span>${escapeHtml(entry.assessment.reasons.join(' · '))}</div>` : ''}
|
|
519
514
|
<div class="line"><span class="label">Provider:</span> ${escapeHtml(entry.item.provider)} · <span class="label">Source:</span> ${escapeHtml(entry.item.source)}</div>
|
|
520
|
-
${
|
|
521
|
-
${sourcePage ? `<div class="line"><span class="label">Page: </span><a class="link" href="${escapeHtml(sourcePage)}" target="_blank" rel="noopener noreferrer">${escapeHtml(sourcePage)}</a></div>` : ''}
|
|
522
|
-
${isManualUrl ? `<div class="line"><span class="label">Install page: </span><a class="link" href="${escapeHtml(manualUrl)}" target="_blank" rel="noopener noreferrer">${escapeHtml(manualUrl)}</a></div>` : ''}
|
|
515
|
+
${buildItemLinks(entry.item).map(l => `<div class="line"><span class="label">${escapeHtml(l.label)}: </span><a class="link" href="${escapeHtml(l.url)}" target="_blank" rel="noopener noreferrer">${escapeHtml(l.url)}</a></div>`).join('')}
|
|
523
516
|
<div class="install-block">
|
|
524
517
|
<div class="install-header">
|
|
525
518
|
<span class="install-label">Install command</span>
|
|
@@ -568,6 +561,43 @@ function asMetadata(value) {
|
|
|
568
561
|
}
|
|
569
562
|
return value;
|
|
570
563
|
}
|
|
564
|
+
function buildItemLinks(item) {
|
|
565
|
+
const meta = asMetadata(item.metadata);
|
|
566
|
+
const links = [];
|
|
567
|
+
const seen = new Set();
|
|
568
|
+
function add(label, url) {
|
|
569
|
+
if (typeof url !== 'string' || !url.startsWith('http'))
|
|
570
|
+
return;
|
|
571
|
+
if (seen.has(url))
|
|
572
|
+
return;
|
|
573
|
+
seen.add(url);
|
|
574
|
+
links.push({ label, url });
|
|
575
|
+
}
|
|
576
|
+
if (item.kind === 'cursor-extension') {
|
|
577
|
+
const vsixId = meta.vsixId;
|
|
578
|
+
if (typeof vsixId === 'string') {
|
|
579
|
+
add('Marketplace', `https://marketplace.visualstudio.com/items?itemName=${vsixId}`);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
if (item.kind === 'gemini-extension') {
|
|
583
|
+
const pkg = meta.npmPackage;
|
|
584
|
+
if (typeof pkg === 'string') {
|
|
585
|
+
add('npm', `https://www.npmjs.com/package/${encodeURIComponent(pkg)}`);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
add('Install page', item.install.url);
|
|
589
|
+
add('Repository', meta.repositoryUrl);
|
|
590
|
+
add('Repository', meta.githubUrl);
|
|
591
|
+
if (typeof meta.sourceRepo === 'string' && meta.sourceRepo.startsWith('http')) {
|
|
592
|
+
add('Repository', meta.sourceRepo);
|
|
593
|
+
}
|
|
594
|
+
else if (typeof meta.sourceRepo === 'string' && meta.sourceRepo.includes('/')) {
|
|
595
|
+
add('Repository', `https://github.com/${meta.sourceRepo}`);
|
|
596
|
+
}
|
|
597
|
+
add('Website', meta.websiteUrl);
|
|
598
|
+
add('Source page', meta.sourcePage);
|
|
599
|
+
return links;
|
|
600
|
+
}
|
|
571
601
|
function escapeHtml(value) {
|
|
572
602
|
return value
|
|
573
603
|
.replaceAll('&', '&')
|
package/package.json
CHANGED