@shnitzel/plugscout 0.3.18 → 0.3.19
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.
- package/dist/interfaces/cli/ui/web-report.js +266 -128
- package/package.json +1 -1
|
@@ -41,7 +41,7 @@ function filterByKinds(items, kinds) {
|
|
|
41
41
|
function renderHtml(rows, allItems, stats, policy) {
|
|
42
42
|
const kindCounts = countByKind(allItems);
|
|
43
43
|
const riskScale = escapeHtml(formatRiskScale(policy));
|
|
44
|
-
const cardsJson = rows.map((entry) => renderDetailCard(entry
|
|
44
|
+
const cardsJson = rows.map((entry) => renderDetailCard(entry)).join('\n');
|
|
45
45
|
return `<!doctype html>
|
|
46
46
|
<html lang="en">
|
|
47
47
|
<head>
|
|
@@ -52,95 +52,138 @@ function renderHtml(rows, allItems, stats, policy) {
|
|
|
52
52
|
:root {
|
|
53
53
|
color-scheme: dark;
|
|
54
54
|
--bg: #050916;
|
|
55
|
-
--panel: #
|
|
56
|
-
--
|
|
57
|
-
--
|
|
58
|
-
--
|
|
59
|
-
--
|
|
55
|
+
--panel: #0d1626;
|
|
56
|
+
--panel2: #111e32;
|
|
57
|
+
--line: #1e3050;
|
|
58
|
+
--text: #e8f0fb;
|
|
59
|
+
--muted: #9ab0cc;
|
|
60
|
+
--ok: #34d058;
|
|
60
61
|
--warn: #f59e0b;
|
|
61
|
-
--bad: #
|
|
62
|
+
--bad: #f06060;
|
|
62
63
|
--accent: #60a5fa;
|
|
64
|
+
--accent2: #818cf8;
|
|
65
|
+
--blocked-border: #7f1d1d;
|
|
63
66
|
}
|
|
64
67
|
* { box-sizing: border-box; }
|
|
68
|
+
.sr-only { position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0; }
|
|
65
69
|
body {
|
|
66
70
|
margin: 0;
|
|
67
|
-
font: 15px/1.
|
|
71
|
+
font: 15px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
68
72
|
color: var(--text);
|
|
69
|
-
background: radial-gradient(
|
|
70
|
-
padding: 28px;
|
|
73
|
+
background: radial-gradient(ellipse at 50% 0%, #0d1f40 0%, var(--bg) 55%);
|
|
74
|
+
padding: 28px 24px;
|
|
71
75
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
76
|
+
a:focus-visible, button:focus-visible, select:focus-visible, input:focus-visible {
|
|
77
|
+
outline: 2px solid var(--accent);
|
|
78
|
+
outline-offset: 2px;
|
|
79
|
+
}
|
|
80
|
+
.wrap { max-width: 1500px; margin: 0 auto; }
|
|
81
|
+
h1 { margin: 0 0 6px; font-size: 30px; letter-spacing: -0.3px; }
|
|
82
|
+
.sub { color: var(--muted); margin: 0 0 20px; font-size: 14px; line-height: 1.6; }
|
|
83
|
+
/* Stat cards */
|
|
75
84
|
.stat-cards {
|
|
76
85
|
display: grid;
|
|
77
|
-
grid-template-columns: repeat(auto-fit, minmax(
|
|
78
|
-
gap:
|
|
79
|
-
margin-bottom:
|
|
86
|
+
grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
|
|
87
|
+
gap: 10px;
|
|
88
|
+
margin-bottom: 16px;
|
|
80
89
|
}
|
|
81
90
|
.stat-card {
|
|
82
91
|
background: var(--panel);
|
|
83
92
|
border: 1px solid var(--line);
|
|
84
|
-
border-radius:
|
|
85
|
-
padding: 14px;
|
|
93
|
+
border-radius: 10px;
|
|
94
|
+
padding: 12px 14px;
|
|
95
|
+
cursor: default;
|
|
96
|
+
transition: border-color 0.12s, background 0.12s;
|
|
86
97
|
}
|
|
87
|
-
.
|
|
88
|
-
.
|
|
98
|
+
.stat-card.clickable { cursor: pointer; }
|
|
99
|
+
.stat-card.clickable:hover { border-color: var(--accent); background: var(--panel2); }
|
|
100
|
+
.stat-card.clickable:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
101
|
+
.stat-card.active { border-color: var(--accent); background: var(--panel2); }
|
|
102
|
+
.k { color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; }
|
|
103
|
+
.v { font-size: 24px; font-weight: 700; margin-top: 3px; color: var(--text); }
|
|
104
|
+
/* Legend */
|
|
89
105
|
.legend {
|
|
90
|
-
margin-bottom:
|
|
106
|
+
margin-bottom: 16px;
|
|
91
107
|
background: var(--panel);
|
|
92
108
|
border: 1px solid var(--line);
|
|
93
|
-
border-radius:
|
|
94
|
-
padding:
|
|
109
|
+
border-radius: 10px;
|
|
110
|
+
padding: 12px 16px;
|
|
95
111
|
font-size: 13px;
|
|
96
112
|
color: var(--muted);
|
|
113
|
+
line-height: 1.7;
|
|
97
114
|
}
|
|
115
|
+
.legend strong { color: var(--text); }
|
|
98
116
|
/* Filter bar */
|
|
99
117
|
.filter-bar {
|
|
100
118
|
background: var(--panel);
|
|
101
119
|
border: 1px solid var(--line);
|
|
102
|
-
border-radius:
|
|
103
|
-
padding:
|
|
104
|
-
margin-bottom:
|
|
120
|
+
border-radius: 10px;
|
|
121
|
+
padding: 12px 16px;
|
|
122
|
+
margin-bottom: 16px;
|
|
105
123
|
display: flex;
|
|
106
124
|
flex-wrap: wrap;
|
|
107
|
-
gap:
|
|
125
|
+
gap: 8px;
|
|
108
126
|
align-items: center;
|
|
109
127
|
}
|
|
110
128
|
.filter-bar input, .filter-bar select {
|
|
111
|
-
background: #
|
|
112
|
-
border: 1px solid #
|
|
113
|
-
border-radius:
|
|
129
|
+
background: #060e1c;
|
|
130
|
+
border: 1px solid #253a58;
|
|
131
|
+
border-radius: 7px;
|
|
114
132
|
color: var(--text);
|
|
115
133
|
padding: 7px 10px;
|
|
116
134
|
font-size: 13px;
|
|
117
135
|
outline: none;
|
|
136
|
+
transition: border-color 0.12s;
|
|
118
137
|
}
|
|
119
|
-
.filter-bar input { flex: 1; min-width:
|
|
138
|
+
.filter-bar input { flex: 1; min-width: 180px; }
|
|
120
139
|
.filter-bar input::placeholder { color: var(--muted); }
|
|
121
140
|
.filter-bar select:focus, .filter-bar input:focus { border-color: var(--accent); }
|
|
141
|
+
.btn-clear {
|
|
142
|
+
background: none;
|
|
143
|
+
border: 1px solid #253a58;
|
|
144
|
+
border-radius: 7px;
|
|
145
|
+
color: var(--muted);
|
|
146
|
+
padding: 7px 12px;
|
|
147
|
+
font-size: 13px;
|
|
148
|
+
cursor: pointer;
|
|
149
|
+
transition: border-color 0.12s, color 0.12s;
|
|
150
|
+
}
|
|
151
|
+
.btn-clear:hover { border-color: var(--accent); color: var(--text); }
|
|
122
152
|
#result-count { color: var(--muted); font-size: 13px; margin-left: auto; white-space: nowrap; }
|
|
123
153
|
/* Grid */
|
|
124
154
|
.detail-grid {
|
|
125
155
|
display: grid;
|
|
126
|
-
grid-template-columns: repeat(auto-fill, minmax(
|
|
127
|
-
gap:
|
|
156
|
+
grid-template-columns: repeat(auto-fill, minmax(370px, 1fr));
|
|
157
|
+
gap: 10px;
|
|
128
158
|
}
|
|
129
159
|
/* Cards */
|
|
130
160
|
.detail-card {
|
|
131
|
-
border: 1px solid #
|
|
161
|
+
border: 1px solid #1e3050;
|
|
132
162
|
border-radius: 10px;
|
|
133
|
-
background: #
|
|
163
|
+
background: #080f1e;
|
|
134
164
|
overflow: hidden;
|
|
135
|
-
transition: border-color 0.
|
|
165
|
+
transition: border-color 0.12s;
|
|
136
166
|
}
|
|
137
167
|
.detail-card:hover { border-color: #3a5a8a; }
|
|
168
|
+
.detail-card.is-blocked {
|
|
169
|
+
border-left: 3px solid var(--blocked-border);
|
|
170
|
+
}
|
|
171
|
+
.detail-card.is-blocked:hover { border-color: #a33; border-left-color: var(--bad); }
|
|
172
|
+
/* Card header is a button for accessibility */
|
|
138
173
|
.card-header {
|
|
174
|
+
display: block;
|
|
175
|
+
width: 100%;
|
|
176
|
+
text-align: left;
|
|
177
|
+
background: none;
|
|
178
|
+
border: none;
|
|
139
179
|
padding: 14px;
|
|
140
180
|
cursor: pointer;
|
|
141
|
-
|
|
181
|
+
color: var(--text);
|
|
182
|
+
font-family: inherit;
|
|
183
|
+
font-size: inherit;
|
|
184
|
+
line-height: inherit;
|
|
142
185
|
}
|
|
143
|
-
.card-header:hover { background: rgba(255,255,255,0.
|
|
186
|
+
.card-header:hover { background: rgba(255,255,255,0.025); }
|
|
144
187
|
.card-title-row {
|
|
145
188
|
display: flex;
|
|
146
189
|
align-items: flex-start;
|
|
@@ -148,32 +191,32 @@ function renderHtml(rows, allItems, stats, policy) {
|
|
|
148
191
|
gap: 8px;
|
|
149
192
|
margin-bottom: 4px;
|
|
150
193
|
}
|
|
151
|
-
.title { margin: 0; font-size:
|
|
194
|
+
.title { margin: 0; font-size: 15px; font-weight: 600; line-height: 1.3; }
|
|
152
195
|
.meta { color: var(--muted); font-size: 12px; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; margin-bottom: 8px; }
|
|
153
|
-
.pill-row { display: flex; flex-wrap: wrap; gap:
|
|
196
|
+
.pill-row { display: flex; flex-wrap: wrap; gap: 5px; margin-bottom: 7px; }
|
|
154
197
|
.pill {
|
|
155
198
|
display: inline-block;
|
|
156
199
|
border-radius: 999px;
|
|
157
|
-
border: 1px solid #
|
|
200
|
+
border: 1px solid #2d4868;
|
|
158
201
|
padding: 2px 9px;
|
|
159
202
|
font-size: 12px;
|
|
160
203
|
color: var(--text);
|
|
161
204
|
white-space: nowrap;
|
|
162
205
|
}
|
|
163
|
-
.ok { color: var(--ok); border-color: #
|
|
164
|
-
.warn { color: var(--warn); border-color: #
|
|
206
|
+
.ok { color: var(--ok); border-color: #14532d; }
|
|
207
|
+
.warn { color: var(--warn); border-color: #78350f; }
|
|
165
208
|
.bad { color: var(--bad); border-color: #7f1d1d; }
|
|
166
209
|
.chips { display: flex; flex-wrap: wrap; gap: 5px; }
|
|
167
210
|
.chip {
|
|
168
|
-
border: 1px solid #
|
|
211
|
+
border: 1px solid #2a4060;
|
|
169
212
|
border-radius: 999px;
|
|
170
213
|
padding: 2px 8px;
|
|
171
|
-
color: #
|
|
214
|
+
color: #bdd2f0;
|
|
172
215
|
font-size: 12px;
|
|
173
216
|
}
|
|
174
217
|
.expand-hint {
|
|
175
218
|
font-size: 11px;
|
|
176
|
-
color: #
|
|
219
|
+
color: #7a9ec4;
|
|
177
220
|
margin-top: 8px;
|
|
178
221
|
text-align: right;
|
|
179
222
|
}
|
|
@@ -182,24 +225,67 @@ function renderHtml(rows, allItems, stats, policy) {
|
|
|
182
225
|
.card-body {
|
|
183
226
|
display: none;
|
|
184
227
|
padding: 0 14px 14px;
|
|
185
|
-
border-top: 1px solid #
|
|
228
|
+
border-top: 1px solid #172538;
|
|
186
229
|
}
|
|
187
230
|
.detail-card.expanded .card-body { display: block; }
|
|
188
|
-
.line { margin-top: 9px; color: var(--text); font-size:
|
|
231
|
+
.line { margin-top: 9px; color: var(--text); font-size: 13.5px; line-height: 1.5; }
|
|
189
232
|
.line .label { color: var(--muted); }
|
|
190
233
|
.link { color: #93c5fd; text-decoration: none; }
|
|
191
234
|
.link:hover { text-decoration: underline; }
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
margin: 10px
|
|
195
|
-
|
|
196
|
-
border: 1px solid #243654;
|
|
235
|
+
/* Install block */
|
|
236
|
+
.install-block {
|
|
237
|
+
margin-top: 10px;
|
|
238
|
+
border: 1px solid #1e3050;
|
|
197
239
|
border-radius: 8px;
|
|
198
|
-
|
|
240
|
+
overflow: hidden;
|
|
241
|
+
}
|
|
242
|
+
.install-header {
|
|
243
|
+
display: flex;
|
|
244
|
+
align-items: center;
|
|
245
|
+
justify-content: space-between;
|
|
246
|
+
padding: 6px 10px;
|
|
247
|
+
background: #0a1626;
|
|
248
|
+
border-bottom: 1px solid #1e3050;
|
|
249
|
+
}
|
|
250
|
+
.install-label { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.06em; }
|
|
251
|
+
.copy-btn {
|
|
252
|
+
background: #1a3050;
|
|
253
|
+
border: 1px solid #2d4868;
|
|
254
|
+
border-radius: 5px;
|
|
255
|
+
color: var(--accent);
|
|
256
|
+
padding: 3px 10px;
|
|
257
|
+
font-size: 12px;
|
|
258
|
+
cursor: pointer;
|
|
259
|
+
transition: background 0.12s;
|
|
260
|
+
}
|
|
261
|
+
.copy-btn:hover { background: #243d60; }
|
|
262
|
+
.copy-btn.copied { color: var(--ok); border-color: #14532d; }
|
|
263
|
+
pre.install-pre {
|
|
264
|
+
margin: 0;
|
|
265
|
+
padding: 9px 12px;
|
|
266
|
+
background: #050c1a;
|
|
199
267
|
overflow-x: auto;
|
|
200
|
-
color: #
|
|
268
|
+
color: #c9deff;
|
|
201
269
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
202
270
|
font-size: 13px;
|
|
271
|
+
white-space: pre-wrap;
|
|
272
|
+
word-break: break-all;
|
|
273
|
+
}
|
|
274
|
+
.plugscout-install {
|
|
275
|
+
margin-top: 8px;
|
|
276
|
+
padding: 8px 12px;
|
|
277
|
+
background: #060e1c;
|
|
278
|
+
border: 1px solid #1e3050;
|
|
279
|
+
border-radius: 8px;
|
|
280
|
+
display: flex;
|
|
281
|
+
align-items: center;
|
|
282
|
+
justify-content: space-between;
|
|
283
|
+
gap: 8px;
|
|
284
|
+
}
|
|
285
|
+
.plugscout-install code {
|
|
286
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
287
|
+
font-size: 13px;
|
|
288
|
+
color: var(--accent);
|
|
203
289
|
}
|
|
204
290
|
.hidden { display: none !important; }
|
|
205
291
|
</style>
|
|
@@ -207,31 +293,38 @@ function renderHtml(rows, allItems, stats, policy) {
|
|
|
207
293
|
<body>
|
|
208
294
|
<div class="wrap">
|
|
209
295
|
<h1>PlugScout Web Report</h1>
|
|
210
|
-
<p class="sub">
|
|
211
|
-
|
|
296
|
+
<p class="sub">
|
|
297
|
+
Claude plugins · Claude connectors · Copilot extensions ·
|
|
298
|
+
Cursor extensions · Gemini extensions · Skills · MCP servers
|
|
299
|
+
</p>
|
|
300
|
+
${stats.shownItems < stats.totalItems ? `<p class="sub">Showing ${stats.shownItems.toLocaleString()} of ${stats.totalItems.toLocaleString()} catalog items · stat card counts reflect full catalog · rerun with <code>--limit ${stats.totalItems}</code> to include all</p>` : ''}
|
|
212
301
|
|
|
213
302
|
<div class="stat-cards">
|
|
214
|
-
<div class="stat-card"><div class="k">Total</div><div class="v">${stats.totalItems.toLocaleString()}</div></div>
|
|
215
|
-
<div class="stat-card"><div class="k">Plugins</div><div class="v">${kindCounts['claude-plugin']}</div></div>
|
|
216
|
-
<div class="stat-card"><div class="k">Connectors</div><div class="v">${kindCounts['claude-connector']}</div></div>
|
|
217
|
-
<div class="stat-card"><div class="k">Copilot
|
|
218
|
-
<div class="stat-card"><div class="k">Cursor
|
|
219
|
-
<div class="stat-card"><div class="k">Gemini
|
|
220
|
-
<div class="stat-card"><div class="k">Skills</div><div class="v">${kindCounts.skill}</div></div>
|
|
221
|
-
<div class="stat-card"><div class="k">MCP Servers</div><div class="v">${kindCounts.mcp}</div></div>
|
|
303
|
+
<div class="stat-card"><div class="k">Total catalog</div><div class="v">${stats.totalItems.toLocaleString()}</div></div>
|
|
304
|
+
<div class="stat-card clickable" role="button" tabindex="0" onclick="setKindFilter('claude-plugin')" onkeydown="if(event.key==='Enter'||event.key===' ')setKindFilter('claude-plugin')"><div class="k">Claude Plugins</div><div class="v">${kindCounts['claude-plugin'].toLocaleString()}</div></div>
|
|
305
|
+
<div class="stat-card clickable" role="button" tabindex="0" onclick="setKindFilter('claude-connector')" onkeydown="if(event.key==='Enter'||event.key===' ')setKindFilter('claude-connector')"><div class="k">Claude Connectors</div><div class="v">${kindCounts['claude-connector'].toLocaleString()}</div></div>
|
|
306
|
+
<div class="stat-card clickable" role="button" tabindex="0" onclick="setKindFilter('copilot-extension')" onkeydown="if(event.key==='Enter'||event.key===' ')setKindFilter('copilot-extension')"><div class="k">Copilot Extensions</div><div class="v">${kindCounts['copilot-extension'].toLocaleString()}</div></div>
|
|
307
|
+
<div class="stat-card clickable" role="button" tabindex="0" onclick="setKindFilter('cursor-extension')" onkeydown="if(event.key==='Enter'||event.key===' ')setKindFilter('cursor-extension')"><div class="k">Cursor Extensions</div><div class="v">${kindCounts['cursor-extension'].toLocaleString()}</div></div>
|
|
308
|
+
<div class="stat-card clickable" role="button" tabindex="0" onclick="setKindFilter('gemini-extension')" onkeydown="if(event.key==='Enter'||event.key===' ')setKindFilter('gemini-extension')"><div class="k">Gemini Extensions</div><div class="v">${kindCounts['gemini-extension'].toLocaleString()}</div></div>
|
|
309
|
+
<div class="stat-card clickable" role="button" tabindex="0" onclick="setKindFilter('skill')" onkeydown="if(event.key==='Enter'||event.key===' ')setKindFilter('skill')"><div class="k">Skills</div><div class="v">${kindCounts.skill.toLocaleString()}</div></div>
|
|
310
|
+
<div class="stat-card clickable" role="button" tabindex="0" onclick="setKindFilter('mcp')" onkeydown="if(event.key==='Enter'||event.key===' ')setKindFilter('mcp')"><div class="k">MCP Servers</div><div class="v">${kindCounts.mcp.toLocaleString()}</div></div>
|
|
222
311
|
<div class="stat-card"><div class="k">Whitelist / Quarantine</div><div class="v">${stats.whitelist} / ${stats.quarantined}</div></div>
|
|
223
312
|
</div>
|
|
224
313
|
|
|
225
|
-
<
|
|
226
|
-
<strong>
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
314
|
+
<details class="legend">
|
|
315
|
+
<summary><strong>Score & risk legend</strong> <span style="font-weight:normal;color:var(--muted)">(click to expand)</span></summary>
|
|
316
|
+
<div style="margin-top:8px">
|
|
317
|
+
<strong>Trust</strong> 0–100 — higher = more trustworthy (provenance, maintenance, adoption)<br>
|
|
318
|
+
<strong>Risk</strong> 0–100 — lower = safer · ${riskScale}<br>
|
|
319
|
+
<span style="color:var(--bad)">■</span> <strong>Blocked</strong> = high/critical risk or quarantined · left red border on card<br>
|
|
320
|
+
<span style="color:var(--ok)">■</span> <strong>Allowed</strong> = passes policy · <span style="color:var(--warn)">■</span> <strong>Approved</strong> = manually whitelisted
|
|
321
|
+
</div>
|
|
322
|
+
</details>
|
|
232
323
|
|
|
233
|
-
<div class="filter-bar">
|
|
234
|
-
<
|
|
324
|
+
<div class="filter-bar" role="search">
|
|
325
|
+
<label class="sr-only" for="search">Search catalog</label>
|
|
326
|
+
<input id="search" type="search" placeholder="Search by name, ID, or capability…" oninput="applyFilters()" autocomplete="off" />
|
|
327
|
+
<label class="sr-only" for="kind-filter">Filter by kind</label>
|
|
235
328
|
<select id="kind-filter" onchange="applyFilters()">
|
|
236
329
|
<option value="">All kinds</option>
|
|
237
330
|
<option value="claude-plugin">Claude plugin</option>
|
|
@@ -242,36 +335,63 @@ function renderHtml(rows, allItems, stats, policy) {
|
|
|
242
335
|
<option value="skill">Skill</option>
|
|
243
336
|
<option value="mcp">MCP server</option>
|
|
244
337
|
</select>
|
|
338
|
+
<label class="sr-only" for="risk-filter">Filter by risk tier</label>
|
|
245
339
|
<select id="risk-filter" onchange="applyFilters()">
|
|
246
340
|
<option value="">All risk tiers</option>
|
|
247
|
-
<option value="low">Low</option>
|
|
248
|
-
<option value="medium">Medium</option>
|
|
249
|
-
<option value="high">High</option>
|
|
250
|
-
<option value="critical">Critical</option>
|
|
341
|
+
<option value="low">Low risk</option>
|
|
342
|
+
<option value="medium">Medium risk</option>
|
|
343
|
+
<option value="high">High risk</option>
|
|
344
|
+
<option value="critical">Critical risk</option>
|
|
251
345
|
</select>
|
|
346
|
+
<label class="sr-only" for="status-filter">Filter by status</label>
|
|
252
347
|
<select id="status-filter" onchange="applyFilters()">
|
|
253
348
|
<option value="">All statuses</option>
|
|
254
349
|
<option value="allowed">Allowed</option>
|
|
255
|
-
<option value="approved">
|
|
350
|
+
<option value="approved">Whitelisted</option>
|
|
256
351
|
<option value="blocked">Blocked</option>
|
|
257
352
|
</select>
|
|
353
|
+
<label class="sr-only" for="sort-by">Sort by</label>
|
|
258
354
|
<select id="sort-by" onchange="applyFilters()">
|
|
259
|
-
<option value="name">
|
|
260
|
-
<option value="trust-desc">
|
|
261
|
-
<option value="risk-asc">
|
|
262
|
-
<option value="risk-desc">
|
|
355
|
+
<option value="name">Name A–Z</option>
|
|
356
|
+
<option value="trust-desc">Trust ↓ (most trusted first)</option>
|
|
357
|
+
<option value="risk-asc">Risk ↑ (safest first)</option>
|
|
358
|
+
<option value="risk-desc">Risk ↓ (riskiest first)</option>
|
|
263
359
|
</select>
|
|
264
|
-
<
|
|
360
|
+
<button class="btn-clear" onclick="clearFilters()" type="button">Clear filters</button>
|
|
361
|
+
<span id="result-count" aria-live="polite" aria-atomic="true"></span>
|
|
265
362
|
</div>
|
|
266
363
|
|
|
267
|
-
<div class="detail-grid" id="card-grid">
|
|
364
|
+
<div class="detail-grid" id="card-grid" role="list">
|
|
268
365
|
${cardsJson}
|
|
269
366
|
</div>
|
|
270
367
|
</div>
|
|
271
368
|
|
|
272
369
|
<script>
|
|
273
|
-
function toggleCard(
|
|
274
|
-
|
|
370
|
+
function toggleCard(btn) {
|
|
371
|
+
const card = btn.closest('.detail-card');
|
|
372
|
+
const expanded = card.classList.toggle('expanded');
|
|
373
|
+
btn.setAttribute('aria-expanded', String(expanded));
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function setKindFilter(kind) {
|
|
377
|
+
document.getElementById('kind-filter').value = kind;
|
|
378
|
+
// Toggle: clicking active kind card resets to all
|
|
379
|
+
const cards = document.querySelectorAll('.stat-card.clickable');
|
|
380
|
+
cards.forEach(c => {
|
|
381
|
+
const onclick = c.getAttribute('onclick') || '';
|
|
382
|
+
c.classList.toggle('active', onclick.includes("'" + kind + "'"));
|
|
383
|
+
});
|
|
384
|
+
applyFilters();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function clearFilters() {
|
|
388
|
+
document.getElementById('search').value = '';
|
|
389
|
+
document.getElementById('kind-filter').value = '';
|
|
390
|
+
document.getElementById('risk-filter').value = '';
|
|
391
|
+
document.getElementById('status-filter').value = '';
|
|
392
|
+
document.getElementById('sort-by').value = 'name';
|
|
393
|
+
document.querySelectorAll('.stat-card.clickable').forEach(c => c.classList.remove('active'));
|
|
394
|
+
applyFilters();
|
|
275
395
|
}
|
|
276
396
|
|
|
277
397
|
function applyFilters() {
|
|
@@ -295,11 +415,10 @@ function renderHtml(rows, allItems, stats, policy) {
|
|
|
295
415
|
if (show) visible++;
|
|
296
416
|
});
|
|
297
417
|
|
|
298
|
-
document.getElementById('result-count').textContent =
|
|
418
|
+
document.getElementById('result-count').textContent = visible.toLocaleString() + ' of ' + cards.length.toLocaleString() + ' shown';
|
|
299
419
|
|
|
300
420
|
const shown = cards.filter(c => !c.classList.contains('hidden'));
|
|
301
421
|
shown.sort((a, b) => {
|
|
302
|
-
if (sort === 'name') return a.dataset.name.localeCompare(b.dataset.name);
|
|
303
422
|
if (sort === 'trust-desc') return Number(b.dataset.trust) - Number(a.dataset.trust);
|
|
304
423
|
if (sort === 'risk-asc') return Number(a.dataset.riskscore) - Number(b.dataset.riskscore);
|
|
305
424
|
if (sort === 'risk-desc') return Number(b.dataset.riskscore) - Number(a.dataset.riskscore);
|
|
@@ -308,17 +427,35 @@ function renderHtml(rows, allItems, stats, policy) {
|
|
|
308
427
|
shown.forEach(card => grid.appendChild(card));
|
|
309
428
|
}
|
|
310
429
|
|
|
311
|
-
|
|
430
|
+
function copyText(text, btn) {
|
|
431
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
432
|
+
const orig = btn.textContent;
|
|
433
|
+
btn.textContent = 'Copied!';
|
|
434
|
+
btn.classList.add('copied');
|
|
435
|
+
setTimeout(() => { btn.textContent = orig; btn.classList.remove('copied'); }, 1800);
|
|
436
|
+
}).catch(() => {
|
|
437
|
+
// Fallback: select text in pre
|
|
438
|
+
const pre = btn.closest('.install-block')?.querySelector('pre') ||
|
|
439
|
+
btn.closest('.plugscout-install')?.querySelector('code');
|
|
440
|
+
if (pre) {
|
|
441
|
+
const sel = window.getSelection();
|
|
442
|
+
const range = document.createRange();
|
|
443
|
+
range.selectNodeContents(pre);
|
|
444
|
+
sel.removeAllRanges();
|
|
445
|
+
sel.addRange(range);
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Initial render
|
|
312
451
|
applyFilters();
|
|
313
452
|
</script>
|
|
314
453
|
</body>
|
|
315
454
|
</html>`;
|
|
316
455
|
}
|
|
317
|
-
function renderDetailCard(entry
|
|
456
|
+
function renderDetailCard(entry) {
|
|
318
457
|
const metadata = asMetadata(entry.item.metadata);
|
|
319
458
|
const trustScore = computeTrustScore(entry.item);
|
|
320
|
-
const confidence = stringOr(metadata.sourceConfidence, 'official');
|
|
321
|
-
const catalogType = stringOr(metadata.catalogType, 'standard');
|
|
322
459
|
const sourceRepo = typeof metadata.sourceRepo === 'string' ? metadata.sourceRepo : '';
|
|
323
460
|
const sourcePage = typeof metadata.sourcePage === 'string' ? metadata.sourcePage : '';
|
|
324
461
|
const status = entry.blocked ? 'blocked' : entry.approved ? 'approved' : 'allowed';
|
|
@@ -332,7 +469,12 @@ function renderDetailCard(entry, policy) {
|
|
|
332
469
|
: entry.assessment.riskTier === 'medium'
|
|
333
470
|
? 'warn'
|
|
334
471
|
: 'bad';
|
|
472
|
+
const safeId = entry.item.id.replace(/[^a-zA-Z0-9-_]/g, '_');
|
|
473
|
+
const bodyId = `body-${safeId}`;
|
|
335
474
|
const searchKey = escapeHtml(`${entry.item.id} ${entry.item.name} ${entry.item.capabilities.join(' ')}`.toLowerCase());
|
|
475
|
+
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) : '';
|
|
336
478
|
const previewChips = entry.item.capabilities
|
|
337
479
|
.slice(0, 3)
|
|
338
480
|
.map((cap) => `<span class="chip">${escapeHtml(cap)}</span>`)
|
|
@@ -340,7 +482,7 @@ function renderDetailCard(entry, policy) {
|
|
|
340
482
|
const allChips = entry.item.capabilities.length > 0
|
|
341
483
|
? entry.item.capabilities.map((cap) => `<span class="chip">${escapeHtml(cap)}</span>`).join('')
|
|
342
484
|
: '<span class="chip">no capability tags</span>';
|
|
343
|
-
return `<article class="detail-card"
|
|
485
|
+
return `<article class="detail-card${entry.blocked ? ' is-blocked' : ''}" role="listitem"
|
|
344
486
|
data-search="${searchKey}"
|
|
345
487
|
data-kind="${escapeHtml(entry.item.kind)}"
|
|
346
488
|
data-risk="${escapeHtml(entry.assessment.riskTier)}"
|
|
@@ -348,7 +490,7 @@ function renderDetailCard(entry, policy) {
|
|
|
348
490
|
data-name="${escapeHtml(entry.item.name.toLowerCase())}"
|
|
349
491
|
data-trust="${trustScore.toFixed(0)}"
|
|
350
492
|
data-riskscore="${entry.assessment.riskScore.toFixed(0)}">
|
|
351
|
-
<
|
|
493
|
+
<button class="card-header" onclick="toggleCard(this)" aria-expanded="false" aria-controls="${bodyId}" type="button">
|
|
352
494
|
<div class="card-title-row">
|
|
353
495
|
<h3 class="title">${escapeHtml(entry.item.name)}</h3>
|
|
354
496
|
<span class="pill">${escapeHtml(entry.item.kind)}</span>
|
|
@@ -356,25 +498,39 @@ function renderDetailCard(entry, policy) {
|
|
|
356
498
|
<div class="meta">${escapeHtml(entry.item.id)}</div>
|
|
357
499
|
<div class="pill-row">
|
|
358
500
|
<span class="pill">trust: ${trustScore.toFixed(0)}</span>
|
|
359
|
-
<span class="pill ${riskClass}">risk: ${escapeHtml(entry.assessment.riskTier)}
|
|
501
|
+
<span class="pill ${riskClass}">risk: ${escapeHtml(entry.assessment.riskTier)} (${entry.assessment.riskScore.toFixed(0)})</span>
|
|
360
502
|
<span class="pill ${statusClass}">${escapeHtml(status)}</span>
|
|
361
503
|
</div>
|
|
362
504
|
${previewChips ? `<div class="chips">${previewChips}</div>` : ''}
|
|
363
|
-
<div class="expand-hint">▼
|
|
364
|
-
</
|
|
365
|
-
<div class="card-body">
|
|
505
|
+
<div class="expand-hint">▼ details</div>
|
|
506
|
+
</button>
|
|
507
|
+
<div class="card-body" id="${bodyId}">
|
|
366
508
|
<div class="line">${escapeHtml(entry.item.description)}</div>
|
|
367
|
-
${benefitSummary ? `<div class="line"><span class="label">What it does
|
|
368
|
-
${bestFor.length > 0 ? `<div class="line"><span class="label">Best for
|
|
369
|
-
${tradeoffs.length > 0 ? `<div class="line"><span class="label">Tradeoffs
|
|
370
|
-
|
|
371
|
-
<div class="
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
${
|
|
377
|
-
<
|
|
509
|
+
${benefitSummary ? `<div class="line"><span class="label">What it does: </span>${escapeHtml(benefitSummary)}</div>` : ''}
|
|
510
|
+
${bestFor.length > 0 ? `<div class="line"><span class="label">Best for: </span>${escapeHtml(bestFor.join(' · '))}</div>` : ''}
|
|
511
|
+
${tradeoffs.length > 0 ? `<div class="line"><span class="label">Tradeoffs: </span>${escapeHtml(tradeoffs.join(' · '))}</div>` : ''}
|
|
512
|
+
${entry.item.capabilities.length > 0 ? `<div class="line"><span class="label">Capabilities: </span></div><div class="chips" style="margin-top:6px">${allChips}</div>` : ''}
|
|
513
|
+
<div class="line">
|
|
514
|
+
<span class="label">Trust:</span> ${trustScore.toFixed(0)}/100 (${escapeHtml(describeTrustBand(trustScore))}) ·
|
|
515
|
+
<span class="label">Risk:</span> ${entry.assessment.riskScore.toFixed(0)}/100 — ${escapeHtml(entry.assessment.riskTier)} ·
|
|
516
|
+
<span class="label">Status:</span> ${escapeHtml(status)}
|
|
517
|
+
</div>
|
|
518
|
+
${entry.assessment.reasons.length > 0 ? `<div class="line"><span class="label">Risk signals: </span>${escapeHtml(entry.assessment.reasons.join(' · '))}</div>` : ''}
|
|
519
|
+
<div class="line"><span class="label">Provider:</span> ${escapeHtml(entry.item.provider)} · <span class="label">Source:</span> ${escapeHtml(entry.item.source)}</div>
|
|
520
|
+
${sourceRepo ? `<div class="line"><span class="label">Repo: </span><a class="link" href="${escapeHtml(sourceRepo)}" target="_blank" rel="noopener noreferrer">${escapeHtml(sourceRepo)}</a></div>` : ''}
|
|
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>` : ''}
|
|
523
|
+
<div class="install-block">
|
|
524
|
+
<div class="install-header">
|
|
525
|
+
<span class="install-label">Install command</span>
|
|
526
|
+
<button class="copy-btn" type="button" onclick="copyText(${JSON.stringify(installHint)}, this)">Copy</button>
|
|
527
|
+
</div>
|
|
528
|
+
<pre class="install-pre">${escapeHtml(installHint)}</pre>
|
|
529
|
+
</div>
|
|
530
|
+
<div class="plugscout-install">
|
|
531
|
+
<code>${escapeHtml(plugscoutCmd)}</code>
|
|
532
|
+
<button class="copy-btn" type="button" onclick="copyText(${JSON.stringify(plugscoutCmd)}, this)">Copy</button>
|
|
533
|
+
</div>
|
|
378
534
|
</div>
|
|
379
535
|
</article>`;
|
|
380
536
|
}
|
|
@@ -400,18 +556,6 @@ function describeTrustBand(score) {
|
|
|
400
556
|
}
|
|
401
557
|
return 'needs review';
|
|
402
558
|
}
|
|
403
|
-
function describeRiskBand(score, policy) {
|
|
404
|
-
if (score <= policy.thresholds.lowMax) {
|
|
405
|
-
return 'low-risk zone';
|
|
406
|
-
}
|
|
407
|
-
if (score <= policy.thresholds.mediumMax) {
|
|
408
|
-
return 'medium-risk zone';
|
|
409
|
-
}
|
|
410
|
-
if (score <= policy.thresholds.highMax) {
|
|
411
|
-
return 'high-risk zone';
|
|
412
|
-
}
|
|
413
|
-
return 'critical-risk zone';
|
|
414
|
-
}
|
|
415
559
|
function formatRiskScale(policy) {
|
|
416
560
|
const low = policy.thresholds.lowMax;
|
|
417
561
|
const medium = policy.thresholds.mediumMax;
|
|
@@ -424,12 +568,6 @@ function asMetadata(value) {
|
|
|
424
568
|
}
|
|
425
569
|
return value;
|
|
426
570
|
}
|
|
427
|
-
function stringOr(value, fallback) {
|
|
428
|
-
if (typeof value === 'string' && value.trim().length > 0) {
|
|
429
|
-
return value;
|
|
430
|
-
}
|
|
431
|
-
return fallback;
|
|
432
|
-
}
|
|
433
571
|
function escapeHtml(value) {
|
|
434
572
|
return value
|
|
435
573
|
.replaceAll('&', '&')
|
package/package.json
CHANGED