@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,
|
|
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(
|
|
28
|
-
lines.push(
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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('
|
|
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('
|
|
47
|
-
lines.push('
|
|
48
|
-
lines.push('
|
|
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(
|
|
67
|
+
return await fs.readFile(getPackagePath(file), 'utf8');
|
|
55
68
|
}
|
|
56
69
|
catch {
|
|
57
|
-
|
|
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
|
|
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
|
-
|
|
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 = '
|
|
190
|
-
const ARROW_DOWN = '
|
|
201
|
+
const ARROW_UP = '[A';
|
|
202
|
+
const ARROW_DOWN = '[B';
|
|
191
203
|
const ENTER = '\r';
|
|
192
|
-
const CTRL_C = '
|
|
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
|
-
|
|
196
|
-
|
|
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 ? '
|
|
202
|
-
|
|
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
|
-
|
|
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