@shnitzel/plugscout 0.3.16 → 0.3.17

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,6 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import fs from 'node:fs/promises';
3
3
  import { createInterface } from 'node:readline/promises';
4
+ import { moveCursor, clearScreenDown } from 'node:readline';
4
5
  import { spawn } from 'node:child_process';
5
6
  import { stdin, stdout } from 'node:process';
6
7
  import { loadItemInsights, loadRegistries, loadSecurityPolicy } from '../../config/runtime.js';
@@ -478,6 +479,7 @@ async function handleList(args) {
478
479
  if (details) {
479
480
  console.log(renderCatalogDecisionDetails(filtered, policy, insights));
480
481
  }
482
+ await promptResultBrowser(filtered.map((e) => ({ id: e.item.id, name: e.item.name })));
481
483
  }
482
484
  async function handleShow(args) {
483
485
  const id = readFlag(args, '--id');
@@ -549,6 +551,7 @@ async function handleSearch(args) {
549
551
  name: entry.item.name
550
552
  }))));
551
553
  printHint('Use `show --id <catalog-id>` for full detail.');
554
+ await promptResultBrowser(matches.map((e) => ({ id: e.item.id, name: e.item.name })));
552
555
  }
553
556
  async function handleExplain(args) {
554
557
  const kinds = readKinds(args);
@@ -616,14 +619,18 @@ async function handleScan(args) {
616
619
  printJson(payload);
617
620
  return;
618
621
  }
619
- console.log('Repository Scan');
620
- console.log(`Archetype: ${scan.inferredArchetype}`);
621
- console.log(`Confidence: ${scan.inferenceConfidence}%`);
622
- console.log(`Stack: ${scan.stack.join(', ') || 'none'}`);
623
- console.log(`Compatibility tags: ${scan.compatibilityTags.join(', ') || 'none'}`);
624
- console.log(`Inferred capabilities: ${scan.inferredCapabilities.join(', ') || 'none'}`);
622
+ const resolvedProject = path.resolve(project);
623
+ const relProject = path.relative(process.cwd(), resolvedProject) || '.';
624
+ const confidenceLabel = scan.inferenceConfidence >= 80 ? 'high' : scan.inferenceConfidence >= 50 ? 'medium' : 'low';
625
+ console.log(`Scanned: ${relProject} (${resolvedProject})`);
626
+ console.log('');
627
+ console.log(` Archetype: ${scan.inferredArchetype} (confidence ${scan.inferenceConfidence}% ${confidenceLabel})`);
628
+ console.log(` Stack: ${scan.stack.join(', ') || 'none detected'}`);
629
+ console.log(` Capabilities: ${scan.inferredCapabilities.join(', ') || 'none inferred'}`);
630
+ console.log(` Tags: ${scan.compatibilityTags.join(', ') || 'none'}`);
625
631
  console.log('');
626
632
  if (scan.archetypeScores.length > 0) {
633
+ console.log('Archetype match scores (how well your project fits each pattern):');
627
634
  console.log(renderTable([
628
635
  { key: 'name', header: 'ARCHETYPE', width: 36 },
629
636
  { key: 'score', header: 'SCORE', width: 8 }
@@ -634,13 +641,15 @@ async function handleScan(args) {
634
641
  console.log('');
635
642
  }
636
643
  if (scan.scanEvidence.length > 0) {
637
- console.log('Evidence');
638
- scan.scanEvidence.slice(0, 16).forEach((line) => console.log(`- ${line}`));
644
+ console.log(`Evidence — ${scan.scanEvidence.length} signal(s) found in ${relProject}:`);
645
+ scan.scanEvidence.slice(0, 16).forEach((line) => console.log(` - ${line}`));
639
646
  if (scan.scanEvidence.length > 16) {
640
- console.log(`- ...and ${scan.scanEvidence.length - 16} more`);
647
+ console.log(` - ...and ${scan.scanEvidence.length - 16} more`);
641
648
  }
649
+ console.log('');
642
650
  }
643
- printHint('Use `recommend --project . --explain-scan` to turn scan signals into ranked recommendations.');
651
+ printHint(`Next: plugscout recommend --project ${relProject} --only-safe --limit 10`);
652
+ printHint('Add --explain-scan to see how these signals shape recommendations.');
644
653
  }
645
654
  async function handleTop(args) {
646
655
  const project = readFlag(args, '--project') ?? '.';
@@ -668,6 +677,7 @@ async function handleTop(args) {
668
677
  const catalogMap = new Map(catalogItems.map((item) => [item.id, item]));
669
678
  console.log(renderRecommendationDecisionDetails(safe, catalogMap, policy, insights));
670
679
  }
680
+ await promptResultBrowser(safe.map((e) => ({ id: e.id, name: '' })));
671
681
  }
672
682
  async function handleSync(args) {
673
683
  const kinds = readKinds(args);
@@ -767,6 +777,9 @@ async function handleRecommend(args) {
767
777
  console.log(`Exported ${ranked.length} recommendations to ${exportPath}`);
768
778
  }
769
779
  printHint('Next: run `show --id <catalog-id>` or `install --id <catalog-id> --yes`.');
780
+ if (format !== 'json') {
781
+ await promptResultBrowser(ranked.map((e) => ({ id: e.id, name: '' })));
782
+ }
770
783
  }
771
784
  async function handleWeb(args) {
772
785
  const out = readFlag(args, '--out') ?? '.plugscout/report.html';
@@ -1127,6 +1140,92 @@ function describeRiskPosture(posture) {
1127
1140
  }
1128
1141
  return 'show full catalog/recommendation set, including blocked items with flags.';
1129
1142
  }
1143
+ async function promptResultBrowser(entries) {
1144
+ if (!process.stdout.isTTY || entries.length === 0)
1145
+ return;
1146
+ const WINDOW = 8;
1147
+ let selected = 0;
1148
+ let linesDrawn = 0;
1149
+ const ARROW_UP = '[A';
1150
+ const ARROW_DOWN = '[B';
1151
+ const ENTER = '\r';
1152
+ const CTRL_C = '';
1153
+ const Q = 'q';
1154
+ const ESC = '\x1b';
1155
+ function visibleStart() {
1156
+ return Math.max(0, Math.min(selected - Math.floor(WINDOW / 2), entries.length - WINDOW));
1157
+ }
1158
+ function renderBrowser(firstRender) {
1159
+ if (!firstRender) {
1160
+ moveCursor(process.stdout, 0, -linesDrawn);
1161
+ clearScreenDown(process.stdout);
1162
+ }
1163
+ let drawn = 0;
1164
+ const start = visibleStart();
1165
+ const end = Math.min(start + WINDOW, entries.length);
1166
+ for (let i = start; i < end; i++) {
1167
+ const prefix = i === selected ? ' ❯ ' : ' ';
1168
+ const nameSuffix = entries[i].name ? ` ${entries[i].name}` : '';
1169
+ const line = `${prefix}${entries[i].id}${nameSuffix}`;
1170
+ process.stdout.write(`${line}\n`);
1171
+ drawn += 1;
1172
+ }
1173
+ if (entries.length > WINDOW) {
1174
+ process.stdout.write(`\x1b[2m (${selected + 1}/${entries.length})\x1b[0m\n`);
1175
+ drawn += 1;
1176
+ }
1177
+ linesDrawn = drawn;
1178
+ }
1179
+ // eslint-disable-next-line prefer-const
1180
+ let running = true;
1181
+ let firstLoop = true;
1182
+ while (running) {
1183
+ if (firstLoop) {
1184
+ process.stdout.write('\x1b[2m Inspect results: ↑↓ navigate ⏎ inspect q skip\x1b[0m\n\n');
1185
+ firstLoop = false;
1186
+ }
1187
+ else {
1188
+ process.stdout.write('\n\x1b[2m Inspect another: ↑↓ navigate ⏎ inspect q skip\x1b[0m\n\n');
1189
+ }
1190
+ linesDrawn = 0;
1191
+ process.stdin.setRawMode(true);
1192
+ process.stdin.resume();
1193
+ process.stdin.setEncoding('utf8');
1194
+ renderBrowser(true);
1195
+ const action = await new Promise((resolve) => {
1196
+ process.stdin.on('data', function onKey(key) {
1197
+ if (key === CTRL_C || key === Q || key === ESC) {
1198
+ process.stdin.removeListener('data', onKey);
1199
+ process.stdin.setRawMode(false);
1200
+ process.stdin.pause();
1201
+ process.stdout.write('\n');
1202
+ resolve({ exit: true });
1203
+ }
1204
+ else if (key === ARROW_UP) {
1205
+ selected = (selected - 1 + entries.length) % entries.length;
1206
+ renderBrowser(false);
1207
+ }
1208
+ else if (key === ARROW_DOWN) {
1209
+ selected = (selected + 1) % entries.length;
1210
+ renderBrowser(false);
1211
+ }
1212
+ else if (key === ENTER) {
1213
+ process.stdin.removeListener('data', onKey);
1214
+ process.stdin.setRawMode(false);
1215
+ process.stdin.pause();
1216
+ process.stdout.write('\n');
1217
+ resolve({ exit: false, id: entries[selected].id });
1218
+ }
1219
+ });
1220
+ });
1221
+ if (action.exit) {
1222
+ running = false;
1223
+ }
1224
+ else if (action.id) {
1225
+ await handleShow(['--id', action.id]);
1226
+ }
1227
+ }
1228
+ }
1130
1229
  function normalizeCommand(raw) {
1131
1230
  const normalized = raw.trim().toLowerCase();
1132
1231
  return COMMAND_ALIASES[normalized] ?? null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shnitzel/plugscout",
3
- "version": "0.3.16",
3
+ "version": "0.3.17",
4
4
  "description": "Claude plugins + Claude connectors + Copilot extensions + Skills + MCP security intelligence framework",
5
5
  "private": false,
6
6
  "type": "module",