@shnitzel/plugscout 0.3.13 → 0.3.15

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.
@@ -0,0 +1,20 @@
1
+ .-----------------------------------------.
2
+ | Scouting plugins so you don't have to. |
3
+ '-----------------------------------------'
4
+ \/
5
+ ___________
6
+ _/ ★ ☆ ★ \_
7
+ /_______________\
8
+ |_________________|
9
+ | (◉) (◉) |
10
+ | ∧ |
11
+ | '~~~~~~~' |
12
+ |_________________|
13
+ _/| [ * ] |\_
14
+ / |_______________| \
15
+ | |
16
+ _| |_
17
+ (_) (_)
18
+
19
+ PlugScout {{version}}
20
+ maintained by {{author}}
@@ -11,7 +11,8 @@ export const colors = {
11
11
  red: (value) => wrap(31, value),
12
12
  cyan: (value) => wrap(36, value),
13
13
  gray: (value) => wrap(90, value),
14
- bold: (value) => wrap(1, value)
14
+ bold: (value) => wrap(1, value),
15
+ dim: (value) => wrap(2, value)
15
16
  };
16
17
  export function colorRisk(tier, value) {
17
18
  if (tier === 'low') {
@@ -1,14 +1,16 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import { spawn } from 'node:child_process';
3
- import { createInterface } from 'node:readline';
3
+ import { createInterface, moveCursor, clearScreenDown } from 'node:readline';
4
4
  import { loadQuarantine, loadWhitelist } from '../../../catalog/repository.js';
5
5
  import { getStaleRegistries, loadSyncState } from '../../../catalog/sync-state.js';
6
6
  import { getPackagePath } from '../../../lib/paths.js';
7
7
  import { colors } from '../formatters/colors.js';
8
8
  import { isSetUp, loadCatalogItems } from '../../../api/index.js';
9
9
  export async function renderHomeScreen() {
10
+ const termCols = process.stdout.columns ?? 80;
11
+ const useCompact = termCols < 82;
10
12
  const [logo, pkg, catalogStats, runtimeStats] = await Promise.all([
11
- readLogo(),
13
+ readLogo(useCompact),
12
14
  readPackageMeta(),
13
15
  readCatalogStats(),
14
16
  readRuntimeStats()
@@ -21,40 +23,56 @@ export async function renderHomeScreen() {
21
23
  .replace('{{author}}', author || 'unknown');
22
24
  lines.push(colorIfTty(renderedLogo.trimEnd(), colors.cyan));
23
25
  lines.push('');
24
- lines.push('Discover and safely install Claude plugins, Claude connectors, Copilot extensions, Skills, and MCP servers.');
26
+ lines.push(colorIfTty('Discover and safely install Claude plugins, connectors,', colors.dim));
27
+ lines.push(colorIfTty('Copilot/Cursor/Gemini extensions, Skills, and MCP servers.', colors.dim));
25
28
  lines.push('');
26
29
  lines.push(colorIfTty('Catalog', colors.bold));
27
- lines.push(`- items=${catalogStats.items} skill=${catalogStats.skill} mcp=${catalogStats.mcp} claude-plugin=${catalogStats.claudePlugin} claude-connector=${catalogStats.claudeConnector} copilot-extension=${catalogStats.copilotExtension}`);
28
- lines.push(`- stale-registries=${runtimeStats.staleRegistries} whitelist=${runtimeStats.whitelist} quarantined=${runtimeStats.quarantined}`);
30
+ lines.push(colorIfTty(` items=${catalogStats.items} skill=${catalogStats.skill} mcp=${catalogStats.mcp} claude-plugin=${catalogStats.claudePlugin} claude-connector=${catalogStats.claudeConnector}`, colors.dim));
31
+ lines.push(colorIfTty(` copilot-extension=${catalogStats.copilotExtension} cursor-extension=${catalogStats.cursorExtension} gemini-extension=${catalogStats.geminiExtension}`, colors.dim));
32
+ lines.push(colorIfTty(` stale-registries=${runtimeStats.staleRegistries} whitelist=${runtimeStats.whitelist} quarantined=${runtimeStats.quarantined}`, colors.dim));
29
33
  lines.push('');
30
34
  lines.push(colorIfTty('Quick actions', colors.bold));
31
- lines.push('- plugscout doctor');
32
- lines.push('- plugscout status --verbose');
33
- lines.push('- plugscout recommend --project . --only-safe --limit 10');
34
- lines.push('- plugscout sync --dry-run');
35
- lines.push('- plugscout help');
35
+ for (const cmd of [
36
+ 'plugscout doctor',
37
+ 'plugscout status --verbose',
38
+ 'plugscout recommend --project . --only-safe --limit 10',
39
+ 'plugscout sync --dry-run',
40
+ 'plugscout help',
41
+ ]) {
42
+ lines.push(` ${colorIfTty(cmd, colors.green)}`);
43
+ }
36
44
  lines.push('');
37
45
  lines.push(colorIfTty('Examples', colors.bold));
38
- lines.push('- plugscout list --kind connectors --limit 10');
39
- lines.push('- plugscout search github');
40
- lines.push('- plugscout show --id claude-connector:asana');
46
+ for (const cmd of [
47
+ 'plugscout list --kind connectors --limit 10',
48
+ 'plugscout list --kind cursor --limit 15',
49
+ 'plugscout search github',
50
+ 'plugscout show --id claude-connector:asana',
51
+ ]) {
52
+ lines.push(` ${colorIfTty(cmd, colors.green)}`);
53
+ }
41
54
  lines.push('');
42
55
  lines.push(colorIfTty('Kind aliases', colors.bold));
43
- lines.push('- skills, mcps, plugins, connectors, extensions');
56
+ lines.push(colorIfTty(' skills · mcps · plugins · connectors · extensions · cursor · gemini', colors.dim));
44
57
  lines.push('');
45
58
  lines.push(colorIfTty('Ranking meaning', colors.bold));
46
- lines.push('- `top` and `recommend` are repo-aware suggestions, not global popularity charts.');
47
- lines.push('- score = fit + trust + freshness - security - blocked');
48
- lines.push('- higher score means a better match for this repo under current policy');
49
- lines.push('- review each suggestion before installing; do not install blindly from rank alone');
59
+ lines.push(colorIfTty(' top/recommend output is repo-aware suggestions, not a global popularity chart', colors.dim));
60
+ lines.push(colorIfTty(' score = fit + trust + freshness - security - blocked', colors.dim));
61
+ lines.push(colorIfTty(' review before installing do not install blindly from rank alone', colors.dim));
50
62
  return lines.join('\n');
51
63
  }
52
- async function readLogo() {
64
+ async function readLogo(compact = false) {
65
+ const file = compact ? 'assets/cli/logo-compact.txt' : 'assets/cli/logo.txt';
53
66
  try {
54
- return await fs.readFile(getPackagePath('assets/cli/logo.txt'), 'utf8');
67
+ return await fs.readFile(getPackagePath(file), 'utf8');
55
68
  }
56
69
  catch {
57
- return 'PlugScout';
70
+ try {
71
+ return await fs.readFile(getPackagePath('assets/cli/logo.txt'), 'utf8');
72
+ }
73
+ catch {
74
+ return 'PlugScout';
75
+ }
58
76
  }
59
77
  }
60
78
  async function readPackageMeta() {
@@ -68,38 +86,32 @@ async function readPackageMeta() {
68
86
  }
69
87
  async function readCatalogStats() {
70
88
  const items = await loadCatalogItems();
71
- let skill = 0;
72
- let mcp = 0;
73
- let claudePlugin = 0;
74
- let claudeConnector = 0;
75
- let copilotExtension = 0;
89
+ let skill = 0, mcp = 0, claudePlugin = 0, claudeConnector = 0;
90
+ let copilotExtension = 0, cursorExtension = 0, geminiExtension = 0;
76
91
  items.forEach((item) => {
77
92
  if (item.kind === 'skill') {
78
93
  skill += 1;
79
- return;
80
94
  }
81
- if (item.kind === 'mcp') {
95
+ else if (item.kind === 'mcp') {
82
96
  mcp += 1;
83
- return;
84
97
  }
85
- if (item.kind === 'claude-plugin') {
98
+ else if (item.kind === 'claude-plugin') {
86
99
  claudePlugin += 1;
87
- return;
88
100
  }
89
- if (item.kind === 'claude-connector') {
101
+ else if (item.kind === 'claude-connector') {
90
102
  claudeConnector += 1;
91
- return;
92
103
  }
93
- copilotExtension += 1;
104
+ else if (item.kind === 'cursor-extension') {
105
+ cursorExtension += 1;
106
+ }
107
+ else if (item.kind === 'gemini-extension') {
108
+ geminiExtension += 1;
109
+ }
110
+ else {
111
+ copilotExtension += 1;
112
+ }
94
113
  });
95
- return {
96
- items: items.length,
97
- skill,
98
- mcp,
99
- claudePlugin,
100
- claudeConnector,
101
- copilotExtension
102
- };
114
+ return { items: items.length, skill, mcp, claudePlugin, claudeConnector, copilotExtension, cursorExtension, geminiExtension };
103
115
  }
104
116
  async function readRuntimeStats() {
105
117
  const [syncState, whitelist, quarantine] = await Promise.all([loadSyncState(), loadWhitelist(), loadQuarantine()]);
@@ -186,33 +198,50 @@ export async function renderInteractiveHome() {
186
198
  const screen = await renderHomeScreen();
187
199
  process.stdout.write(screen + '\n\n');
188
200
  let selected = 0;
189
- const ARROW_UP = '\u001b[A';
190
- const ARROW_DOWN = '\u001b[B';
201
+ const ARROW_UP = '';
202
+ const ARROW_DOWN = '';
191
203
  const ENTER = '\r';
192
- const CTRL_C = '\u0003';
204
+ const CTRL_C = '';
205
+ // Physical lines written by the last render call — used to move the cursor
206
+ // back up accurately regardless of terminal width / line wrapping.
207
+ let linesDrawn = 0;
208
+ function physicalLines(text) {
209
+ const cols = process.stdout.columns || 80;
210
+ // Strip ANSI codes before measuring display width
211
+ // eslint-disable-next-line no-control-regex
212
+ const plain = text.replace(/\x1b\[[^m]*m/g, '');
213
+ if (plain.length === 0)
214
+ return 1;
215
+ return Math.max(1, Math.ceil(plain.length / cols));
216
+ }
193
217
  function render(firstRender) {
194
218
  if (!firstRender) {
195
- // Restore saved cursor position and clear everything below it.
196
- // This avoids line-count arithmetic that breaks when descriptions wrap.
197
- process.stdout.write('\x1b[u\x1b[0J');
219
+ moveCursor(process.stdout, 0, -linesDrawn);
220
+ clearScreenDown(process.stdout);
198
221
  }
222
+ let drawn = 0;
199
223
  for (let i = 0; i < menuItems.length; i++) {
200
224
  const item = menuItems[i];
201
- const prefix = i === selected ? ' \u276f ' : ' ';
202
- process.stdout.write(`${prefix}${item.label}\n`);
225
+ const prefix = i === selected ? ' ' : ' ';
226
+ const labelLine = `${prefix}${item.label}`;
227
+ process.stdout.write(`${labelLine}\n`);
228
+ drawn += physicalLines(labelLine);
203
229
  if (item.description) {
204
230
  const firstLine = item.description.split('\n')[0];
205
- process.stdout.write(` \x1b[2m${firstLine}\x1b[0m\n`);
231
+ const descLine = ` ${firstLine}`;
232
+ process.stdout.write(`\x1b[2m${descLine}\x1b[0m\n`);
233
+ drawn += physicalLines(descLine);
206
234
  }
207
235
  else {
208
236
  process.stdout.write('\n');
237
+ drawn += 1;
209
238
  }
210
239
  }
240
+ linesDrawn = drawn;
211
241
  }
212
242
  let running = true;
213
243
  while (running) {
214
244
  process.stdout.write('\n');
215
- process.stdout.write('\x1b[s'); // save cursor — used by render(false) to redraw in-place
216
245
  process.stdin.setRawMode(true);
217
246
  process.stdin.resume();
218
247
  process.stdin.setEncoding('utf8');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shnitzel/plugscout",
3
- "version": "0.3.13",
3
+ "version": "0.3.15",
4
4
  "description": "Claude plugins + Claude connectors + Copilot extensions + Skills + MCP security intelligence framework",
5
5
  "private": false,
6
6
  "type": "module",