@shnitzel/plugscout 0.3.7 → 0.3.9
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/home.js +65 -49
- package/dist/interfaces/cli/ui/web-report.js +224 -163
- package/package.json +1 -1
|
@@ -183,6 +183,8 @@ export async function renderInteractiveHome() {
|
|
|
183
183
|
process.stdout.write(screen + '\n');
|
|
184
184
|
return;
|
|
185
185
|
}
|
|
186
|
+
const screen = await renderHomeScreen();
|
|
187
|
+
process.stdout.write(screen + '\n\n');
|
|
186
188
|
let selected = 0;
|
|
187
189
|
const ARROW_UP = '\u001b[A';
|
|
188
190
|
const ARROW_DOWN = '\u001b[B';
|
|
@@ -205,60 +207,74 @@ export async function renderInteractiveHome() {
|
|
|
205
207
|
}
|
|
206
208
|
}
|
|
207
209
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
else if (key === ARROW_UP) {
|
|
224
|
-
selected = (selected - 1 + menuItems.length) % menuItems.length;
|
|
225
|
-
render(false);
|
|
226
|
-
}
|
|
227
|
-
else if (key === ARROW_DOWN) {
|
|
228
|
-
selected = (selected + 1) % menuItems.length;
|
|
229
|
-
render(false);
|
|
230
|
-
}
|
|
231
|
-
else if (key === ENTER) {
|
|
232
|
-
process.stdin.removeListener('data', onKey);
|
|
233
|
-
process.stdin.setRawMode(false);
|
|
234
|
-
process.stdin.pause();
|
|
235
|
-
process.stdout.write('\n');
|
|
236
|
-
const item = menuItems[selected];
|
|
237
|
-
if (!item.command) {
|
|
238
|
-
resolve();
|
|
210
|
+
let running = true;
|
|
211
|
+
while (running) {
|
|
212
|
+
process.stdout.write('\n');
|
|
213
|
+
process.stdin.setRawMode(true);
|
|
214
|
+
process.stdin.resume();
|
|
215
|
+
process.stdin.setEncoding('utf8');
|
|
216
|
+
render(true);
|
|
217
|
+
const action = await new Promise((resolve) => {
|
|
218
|
+
process.stdin.on('data', async function onKey(key) {
|
|
219
|
+
if (key === CTRL_C) {
|
|
220
|
+
process.stdin.removeListener('data', onKey);
|
|
221
|
+
process.stdin.setRawMode(false);
|
|
222
|
+
process.stdin.pause();
|
|
223
|
+
process.stdout.write('\n');
|
|
224
|
+
resolve({ exit: true });
|
|
239
225
|
return;
|
|
240
226
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
227
|
+
else if (key === ARROW_UP) {
|
|
228
|
+
selected = (selected - 1 + menuItems.length) % menuItems.length;
|
|
229
|
+
render(false);
|
|
230
|
+
}
|
|
231
|
+
else if (key === ARROW_DOWN) {
|
|
232
|
+
selected = (selected + 1) % menuItems.length;
|
|
233
|
+
render(false);
|
|
234
|
+
}
|
|
235
|
+
else if (key === ENTER) {
|
|
236
|
+
process.stdin.removeListener('data', onKey);
|
|
237
|
+
process.stdin.setRawMode(false);
|
|
238
|
+
process.stdin.pause();
|
|
239
|
+
process.stdout.write('\n');
|
|
240
|
+
const item = menuItems[selected];
|
|
241
|
+
if (!item.command) {
|
|
242
|
+
resolve({ exit: true });
|
|
253
243
|
return;
|
|
254
244
|
}
|
|
255
|
-
args = [...
|
|
245
|
+
let args = [...item.command];
|
|
246
|
+
if (item.needsId) {
|
|
247
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
248
|
+
process.stdin.resume();
|
|
249
|
+
const id = await new Promise((res) => {
|
|
250
|
+
rl.question(' Enter catalog ID: ', (answer) => {
|
|
251
|
+
rl.close();
|
|
252
|
+
res(answer.trim());
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
if (!id) {
|
|
256
|
+
resolve({ exit: false });
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
args = [...args, '--id', id];
|
|
260
|
+
}
|
|
261
|
+
resolve({ exit: false, args });
|
|
256
262
|
}
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
if (action.exit) {
|
|
266
|
+
running = false;
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
if (action.args) {
|
|
257
270
|
const cliPath = getPackagePath('dist/cli.js');
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
271
|
+
await new Promise((done) => {
|
|
272
|
+
const child = spawn(process.execPath, [cliPath, ...action.args], { stdio: 'inherit' });
|
|
273
|
+
child.on('close', () => done());
|
|
274
|
+
child.on('error', () => done());
|
|
275
|
+
});
|
|
261
276
|
}
|
|
262
|
-
|
|
263
|
-
|
|
277
|
+
process.stdout.write('\n');
|
|
278
|
+
}
|
|
279
|
+
}
|
|
264
280
|
}
|
|
@@ -38,12 +38,8 @@ function filterByKinds(items, kinds) {
|
|
|
38
38
|
}
|
|
39
39
|
function renderHtml(rows, stats, policy) {
|
|
40
40
|
const kindCounts = countByKind(rows.map((entry) => entry.item));
|
|
41
|
-
const topClaude = rows.filter((entry) => entry.item.kind === 'claude-plugin').slice(0, 15);
|
|
42
|
-
const topConnectors = rows.filter((entry) => entry.item.kind === 'claude-connector').slice(0, 15);
|
|
43
|
-
const topCopilot = rows.filter((entry) => entry.item.kind === 'copilot-extension').slice(0, 15);
|
|
44
|
-
const allRows = rows.slice(0, 120);
|
|
45
|
-
const detailRows = rows.slice(0, 80);
|
|
46
41
|
const riskScale = escapeHtml(formatRiskScale(policy));
|
|
42
|
+
const cardsJson = rows.map((entry) => renderDetailCard(entry, policy)).join('\n');
|
|
47
43
|
return `<!doctype html>
|
|
48
44
|
<html lang="en">
|
|
49
45
|
<head>
|
|
@@ -74,13 +70,13 @@ function renderHtml(rows, stats, policy) {
|
|
|
74
70
|
.wrap { max-width: 1460px; margin: 0 auto; }
|
|
75
71
|
h1 { margin: 0 0 8px; font-size: 34px; }
|
|
76
72
|
.sub { color: var(--muted); margin: 0 0 22px; }
|
|
77
|
-
.cards {
|
|
73
|
+
.stat-cards {
|
|
78
74
|
display: grid;
|
|
79
|
-
grid-template-columns: repeat(
|
|
75
|
+
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
|
80
76
|
gap: 12px;
|
|
81
77
|
margin-bottom: 18px;
|
|
82
78
|
}
|
|
83
|
-
.card {
|
|
79
|
+
.stat-card {
|
|
84
80
|
background: var(--panel);
|
|
85
81
|
border: 1px solid var(--line);
|
|
86
82
|
border-radius: 12px;
|
|
@@ -88,75 +84,84 @@ function renderHtml(rows, stats, policy) {
|
|
|
88
84
|
}
|
|
89
85
|
.k { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: 0.05em; }
|
|
90
86
|
.v { font-size: 26px; font-weight: 700; margin-top: 4px; }
|
|
91
|
-
.
|
|
92
|
-
margin-
|
|
87
|
+
.legend {
|
|
88
|
+
margin-bottom: 18px;
|
|
93
89
|
background: var(--panel);
|
|
94
90
|
border: 1px solid var(--line);
|
|
95
91
|
border-radius: 12px;
|
|
96
|
-
overflow: hidden;
|
|
97
|
-
}
|
|
98
|
-
.section-body { padding: 14px 16px; }
|
|
99
|
-
.section h2 {
|
|
100
|
-
margin: 0;
|
|
101
92
|
padding: 14px 16px;
|
|
102
|
-
font-size:
|
|
103
|
-
|
|
104
|
-
color: var(--accent);
|
|
93
|
+
font-size: 13px;
|
|
94
|
+
color: var(--muted);
|
|
105
95
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
border-
|
|
111
|
-
|
|
112
|
-
|
|
96
|
+
/* Filter bar */
|
|
97
|
+
.filter-bar {
|
|
98
|
+
background: var(--panel);
|
|
99
|
+
border: 1px solid var(--line);
|
|
100
|
+
border-radius: 12px;
|
|
101
|
+
padding: 14px 16px;
|
|
102
|
+
margin-bottom: 18px;
|
|
103
|
+
display: flex;
|
|
104
|
+
flex-wrap: wrap;
|
|
105
|
+
gap: 10px;
|
|
106
|
+
align-items: center;
|
|
113
107
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
border-radius:
|
|
118
|
-
border: 1px solid #314664;
|
|
119
|
-
padding: 2px 9px;
|
|
120
|
-
font-size: 12px;
|
|
108
|
+
.filter-bar input, .filter-bar select {
|
|
109
|
+
background: #060f1d;
|
|
110
|
+
border: 1px solid #2a3d5c;
|
|
111
|
+
border-radius: 8px;
|
|
121
112
|
color: var(--text);
|
|
122
|
-
|
|
113
|
+
padding: 7px 10px;
|
|
114
|
+
font-size: 13px;
|
|
115
|
+
outline: none;
|
|
123
116
|
}
|
|
124
|
-
.
|
|
125
|
-
.
|
|
126
|
-
.
|
|
127
|
-
|
|
117
|
+
.filter-bar input { flex: 1; min-width: 160px; }
|
|
118
|
+
.filter-bar input::placeholder { color: var(--muted); }
|
|
119
|
+
.filter-bar select:focus, .filter-bar input:focus { border-color: var(--accent); }
|
|
120
|
+
#result-count { color: var(--muted); font-size: 13px; margin-left: auto; white-space: nowrap; }
|
|
121
|
+
/* Grid */
|
|
128
122
|
.detail-grid {
|
|
129
123
|
display: grid;
|
|
130
|
-
grid-template-columns: repeat(auto-
|
|
124
|
+
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
|
|
131
125
|
gap: 12px;
|
|
132
|
-
padding: 14px;
|
|
133
126
|
}
|
|
127
|
+
/* Cards */
|
|
134
128
|
.detail-card {
|
|
135
129
|
border: 1px solid #243654;
|
|
136
130
|
border-radius: 10px;
|
|
137
131
|
background: #081121;
|
|
138
|
-
|
|
132
|
+
overflow: hidden;
|
|
133
|
+
transition: border-color 0.15s;
|
|
134
|
+
}
|
|
135
|
+
.detail-card:hover { border-color: #3a5a8a; }
|
|
136
|
+
.card-header {
|
|
137
|
+
padding: 14px;
|
|
138
|
+
cursor: pointer;
|
|
139
|
+
user-select: none;
|
|
139
140
|
}
|
|
140
|
-
.
|
|
141
|
+
.card-header:hover { background: rgba(255,255,255,0.03); }
|
|
142
|
+
.card-title-row {
|
|
141
143
|
display: flex;
|
|
142
144
|
align-items: flex-start;
|
|
143
145
|
justify-content: space-between;
|
|
144
|
-
gap:
|
|
145
|
-
margin-bottom:
|
|
146
|
-
}
|
|
147
|
-
.title {
|
|
148
|
-
margin: 0;
|
|
149
|
-
font-size: 16px;
|
|
150
|
-
line-height: 1.3;
|
|
146
|
+
gap: 8px;
|
|
147
|
+
margin-bottom: 4px;
|
|
151
148
|
}
|
|
152
|
-
.
|
|
153
|
-
|
|
149
|
+
.title { margin: 0; font-size: 16px; line-height: 1.3; }
|
|
150
|
+
.meta { color: var(--muted); font-size: 12px; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; margin-bottom: 8px; }
|
|
151
|
+
.pill-row { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; }
|
|
152
|
+
.pill {
|
|
153
|
+
display: inline-block;
|
|
154
|
+
border-radius: 999px;
|
|
155
|
+
border: 1px solid #314664;
|
|
156
|
+
padding: 2px 9px;
|
|
154
157
|
font-size: 12px;
|
|
155
|
-
|
|
158
|
+
color: var(--text);
|
|
159
|
+
white-space: nowrap;
|
|
156
160
|
}
|
|
157
|
-
.
|
|
158
|
-
.
|
|
159
|
-
.
|
|
161
|
+
.ok { color: var(--ok); border-color: #166534; }
|
|
162
|
+
.warn { color: var(--warn); border-color: #854d0e; }
|
|
163
|
+
.bad { color: var(--bad); border-color: #7f1d1d; }
|
|
164
|
+
.chips { display: flex; flex-wrap: wrap; gap: 5px; }
|
|
160
165
|
.chip {
|
|
161
166
|
border: 1px solid #2f476b;
|
|
162
167
|
border-radius: 999px;
|
|
@@ -164,102 +169,144 @@ function renderHtml(rows, stats, policy) {
|
|
|
164
169
|
color: #c9dbf5;
|
|
165
170
|
font-size: 12px;
|
|
166
171
|
}
|
|
172
|
+
.expand-hint {
|
|
173
|
+
font-size: 11px;
|
|
174
|
+
color: #4a6a94;
|
|
175
|
+
margin-top: 8px;
|
|
176
|
+
text-align: right;
|
|
177
|
+
}
|
|
178
|
+
.detail-card.expanded .expand-hint { display: none; }
|
|
179
|
+
/* Expanded body */
|
|
180
|
+
.card-body {
|
|
181
|
+
display: none;
|
|
182
|
+
padding: 0 14px 14px;
|
|
183
|
+
border-top: 1px solid #1a2c44;
|
|
184
|
+
}
|
|
185
|
+
.detail-card.expanded .card-body { display: block; }
|
|
186
|
+
.line { margin-top: 9px; color: var(--text); font-size: 14px; }
|
|
187
|
+
.line .label { color: var(--muted); }
|
|
167
188
|
.link { color: #93c5fd; text-decoration: none; }
|
|
168
189
|
.link:hover { text-decoration: underline; }
|
|
190
|
+
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 13px; }
|
|
169
191
|
pre {
|
|
170
|
-
margin:
|
|
192
|
+
margin: 10px 0 0;
|
|
171
193
|
padding: 9px 10px;
|
|
172
194
|
border: 1px solid #243654;
|
|
173
195
|
border-radius: 8px;
|
|
174
196
|
background: #060f1d;
|
|
175
197
|
overflow-x: auto;
|
|
176
198
|
color: #dbeafe;
|
|
199
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
200
|
+
font-size: 13px;
|
|
177
201
|
}
|
|
202
|
+
.hidden { display: none !important; }
|
|
178
203
|
</style>
|
|
179
204
|
</head>
|
|
180
205
|
<body>
|
|
181
206
|
<div class="wrap">
|
|
182
207
|
<h1>PlugScout Web Report</h1>
|
|
183
|
-
<p class="sub">
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
<div class="card"><div class="k">
|
|
187
|
-
<div class="card"><div class="k">
|
|
188
|
-
<div class="card"><div class="k">
|
|
189
|
-
<div class="card"><div class="k">
|
|
190
|
-
<div class="card"><div class="k">
|
|
191
|
-
<div class="card"><div class="k">
|
|
208
|
+
<p class="sub">Claude plugins · Claude connectors · Copilot extensions · Skills · MCP servers</p>
|
|
209
|
+
|
|
210
|
+
<div class="stat-cards">
|
|
211
|
+
<div class="stat-card"><div class="k">Total</div><div class="v">${stats.totalItems}</div></div>
|
|
212
|
+
<div class="stat-card"><div class="k">Plugins</div><div class="v">${kindCounts['claude-plugin']}</div></div>
|
|
213
|
+
<div class="stat-card"><div class="k">Connectors</div><div class="v">${kindCounts['claude-connector']}</div></div>
|
|
214
|
+
<div class="stat-card"><div class="k">Copilot Ext</div><div class="v">${kindCounts['copilot-extension']}</div></div>
|
|
215
|
+
<div class="stat-card"><div class="k">Skills</div><div class="v">${kindCounts.skill}</div></div>
|
|
216
|
+
<div class="stat-card"><div class="k">MCP Servers</div><div class="v">${kindCounts.mcp}</div></div>
|
|
217
|
+
<div class="stat-card"><div class="k">Whitelist / Quarantine</div><div class="v">${stats.whitelist} / ${stats.quarantined}</div></div>
|
|
218
|
+
</div>
|
|
219
|
+
|
|
220
|
+
<div class="legend">
|
|
221
|
+
<strong>Scores:</strong>
|
|
222
|
+
Trust 0–100 (higher = more trustworthy) ·
|
|
223
|
+
Risk 0–100 (lower = safer) ·
|
|
224
|
+
${riskScale} ·
|
|
225
|
+
<span style="color:var(--bad)">blocked</span> = high/critical risk or quarantined
|
|
226
|
+
</div>
|
|
227
|
+
|
|
228
|
+
<div class="filter-bar">
|
|
229
|
+
<input id="search" type="text" placeholder="Search by name or ID…" oninput="applyFilters()" />
|
|
230
|
+
<select id="kind-filter" onchange="applyFilters()">
|
|
231
|
+
<option value="">All kinds</option>
|
|
232
|
+
<option value="claude-plugin">Claude plugin</option>
|
|
233
|
+
<option value="claude-connector">Claude connector</option>
|
|
234
|
+
<option value="copilot-extension">Copilot extension</option>
|
|
235
|
+
<option value="skill">Skill</option>
|
|
236
|
+
<option value="mcp">MCP server</option>
|
|
237
|
+
</select>
|
|
238
|
+
<select id="risk-filter" onchange="applyFilters()">
|
|
239
|
+
<option value="">All risk tiers</option>
|
|
240
|
+
<option value="low">Low</option>
|
|
241
|
+
<option value="medium">Medium</option>
|
|
242
|
+
<option value="high">High</option>
|
|
243
|
+
<option value="critical">Critical</option>
|
|
244
|
+
</select>
|
|
245
|
+
<select id="status-filter" onchange="applyFilters()">
|
|
246
|
+
<option value="">All statuses</option>
|
|
247
|
+
<option value="allowed">Allowed</option>
|
|
248
|
+
<option value="approved">Approved</option>
|
|
249
|
+
<option value="blocked">Blocked</option>
|
|
250
|
+
</select>
|
|
251
|
+
<select id="sort-by" onchange="applyFilters()">
|
|
252
|
+
<option value="name">Sort: Name A–Z</option>
|
|
253
|
+
<option value="trust-desc">Sort: Trust ↓</option>
|
|
254
|
+
<option value="risk-asc">Sort: Risk ↑ (safest first)</option>
|
|
255
|
+
<option value="risk-desc">Sort: Risk ↓ (riskiest first)</option>
|
|
256
|
+
</select>
|
|
257
|
+
<span id="result-count"></span>
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
<div class="detail-grid" id="card-grid">
|
|
261
|
+
${cardsJson}
|
|
192
262
|
</div>
|
|
193
|
-
<section class="section">
|
|
194
|
-
<h2>How to read scores</h2>
|
|
195
|
-
<div class="section-body">
|
|
196
|
-
<div><span class="pill ok">trust</span> 0-100, higher is better.</div>
|
|
197
|
-
<div style="margin-top:6px;"><span class="pill warn">risk</span> 0-100, lower is safer. ${riskScale}</div>
|
|
198
|
-
<div style="margin-top:6px;"><span class="pill bad">blocked</span> means policy high/critical risk or quarantined.</div>
|
|
199
|
-
</div>
|
|
200
|
-
</section>
|
|
201
|
-
${renderTableSection('Top Claude Plugins', topClaude)}
|
|
202
|
-
${renderTableSection('Top Claude Connectors', topConnectors)}
|
|
203
|
-
${renderTableSection('Top Copilot Extensions', topCopilot)}
|
|
204
|
-
${renderTableSection('Catalog Snapshot', allRows)}
|
|
205
|
-
${renderDetailSection('Decision details per item', detailRows, policy)}
|
|
206
263
|
</div>
|
|
264
|
+
|
|
265
|
+
<script>
|
|
266
|
+
function toggleCard(header) {
|
|
267
|
+
header.closest('.detail-card').classList.toggle('expanded');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function applyFilters() {
|
|
271
|
+
const search = document.getElementById('search').value.toLowerCase().trim();
|
|
272
|
+
const kind = document.getElementById('kind-filter').value;
|
|
273
|
+
const risk = document.getElementById('risk-filter').value;
|
|
274
|
+
const status = document.getElementById('status-filter').value;
|
|
275
|
+
const sort = document.getElementById('sort-by').value;
|
|
276
|
+
|
|
277
|
+
const grid = document.getElementById('card-grid');
|
|
278
|
+
const cards = Array.from(grid.querySelectorAll('.detail-card'));
|
|
279
|
+
|
|
280
|
+
let visible = 0;
|
|
281
|
+
cards.forEach(card => {
|
|
282
|
+
const matchSearch = !search || card.dataset.search.includes(search);
|
|
283
|
+
const matchKind = !kind || card.dataset.kind === kind;
|
|
284
|
+
const matchRisk = !risk || card.dataset.risk === risk;
|
|
285
|
+
const matchStatus = !status || card.dataset.status === status;
|
|
286
|
+
const show = matchSearch && matchKind && matchRisk && matchStatus;
|
|
287
|
+
card.classList.toggle('hidden', !show);
|
|
288
|
+
if (show) visible++;
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
document.getElementById('result-count').textContent = 'Showing ' + visible + ' of ' + cards.length;
|
|
292
|
+
|
|
293
|
+
const shown = cards.filter(c => !c.classList.contains('hidden'));
|
|
294
|
+
shown.sort((a, b) => {
|
|
295
|
+
if (sort === 'name') return a.dataset.name.localeCompare(b.dataset.name);
|
|
296
|
+
if (sort === 'trust-desc') return Number(b.dataset.trust) - Number(a.dataset.trust);
|
|
297
|
+
if (sort === 'risk-asc') return Number(a.dataset.riskscore) - Number(b.dataset.riskscore);
|
|
298
|
+
if (sort === 'risk-desc') return Number(b.dataset.riskscore) - Number(a.dataset.riskscore);
|
|
299
|
+
return a.dataset.name.localeCompare(b.dataset.name);
|
|
300
|
+
});
|
|
301
|
+
shown.forEach(card => grid.appendChild(card));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Initial count
|
|
305
|
+
applyFilters();
|
|
306
|
+
</script>
|
|
207
307
|
</body>
|
|
208
308
|
</html>`;
|
|
209
309
|
}
|
|
210
|
-
function renderTableSection(title, rows) {
|
|
211
|
-
return `<section class="section">
|
|
212
|
-
<h2>${escapeHtml(title)}</h2>
|
|
213
|
-
<table>
|
|
214
|
-
<thead>
|
|
215
|
-
<tr>
|
|
216
|
-
<th>ID</th>
|
|
217
|
-
<th>Name</th>
|
|
218
|
-
<th>Kind</th>
|
|
219
|
-
<th>Provider</th>
|
|
220
|
-
<th>Trust</th>
|
|
221
|
-
<th>Source</th>
|
|
222
|
-
<th>Confidence</th>
|
|
223
|
-
<th>Risk</th>
|
|
224
|
-
<th>Status</th>
|
|
225
|
-
</tr>
|
|
226
|
-
</thead>
|
|
227
|
-
<tbody>
|
|
228
|
-
${rows
|
|
229
|
-
.map((entry) => {
|
|
230
|
-
const metadata = asMetadata(entry.item.metadata);
|
|
231
|
-
const confidence = stringOr(metadata.sourceConfidence, 'official');
|
|
232
|
-
const trustScore = computeTrustScore(entry.item);
|
|
233
|
-
const riskClass = entry.assessment.riskTier === 'low'
|
|
234
|
-
? 'ok'
|
|
235
|
-
: entry.assessment.riskTier === 'medium'
|
|
236
|
-
? 'warn'
|
|
237
|
-
: 'bad';
|
|
238
|
-
return `<tr>
|
|
239
|
-
<td class="mono">${escapeHtml(entry.item.id)}</td>
|
|
240
|
-
<td>${escapeHtml(entry.item.name)}</td>
|
|
241
|
-
<td>${escapeHtml(entry.item.kind)}</td>
|
|
242
|
-
<td>${escapeHtml(entry.item.provider)}</td>
|
|
243
|
-
<td><span class="pill">${trustScore.toFixed(0)}</span></td>
|
|
244
|
-
<td>${escapeHtml(entry.item.source)}</td>
|
|
245
|
-
<td><span class="pill">${escapeHtml(confidence)}</span></td>
|
|
246
|
-
<td><span class="pill ${riskClass}">${escapeHtml(entry.assessment.riskTier)} (${entry.assessment.riskScore.toFixed(0)})</span></td>
|
|
247
|
-
<td>${entry.blocked ? '<span class="pill bad">blocked</span>' : entry.approved ? '<span class="pill ok">approved</span>' : '<span class="pill ok">allowed</span>'}</td>
|
|
248
|
-
</tr>`;
|
|
249
|
-
})
|
|
250
|
-
.join('\n')}
|
|
251
|
-
</tbody>
|
|
252
|
-
</table>
|
|
253
|
-
</section>`;
|
|
254
|
-
}
|
|
255
|
-
function renderDetailSection(title, rows, policy) {
|
|
256
|
-
return `<section class="section">
|
|
257
|
-
<h2>${escapeHtml(title)}</h2>
|
|
258
|
-
<div class="detail-grid">
|
|
259
|
-
${rows.map((entry) => renderDetailCard(entry, policy)).join('\n')}
|
|
260
|
-
</div>
|
|
261
|
-
</section>`;
|
|
262
|
-
}
|
|
263
310
|
function renderDetailCard(entry, policy) {
|
|
264
311
|
const metadata = asMetadata(entry.item.metadata);
|
|
265
312
|
const trustScore = computeTrustScore(entry.item);
|
|
@@ -268,48 +315,62 @@ function renderDetailCard(entry, policy) {
|
|
|
268
315
|
const sourceRepo = typeof metadata.sourceRepo === 'string' ? metadata.sourceRepo : '';
|
|
269
316
|
const sourcePage = typeof metadata.sourcePage === 'string' ? metadata.sourcePage : '';
|
|
270
317
|
const status = entry.blocked ? 'blocked' : entry.approved ? 'approved' : 'allowed';
|
|
318
|
+
const statusClass = entry.blocked ? 'bad' : 'ok';
|
|
271
319
|
const installHint = buildInstallHint(entry.item);
|
|
272
320
|
const bestFor = entry.insight?.bestFor ?? [];
|
|
273
321
|
const tradeoffs = entry.insight?.tradeoffs ?? [];
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
322
|
+
const benefitSummary = entry.insight?.benefitSummary ?? '';
|
|
323
|
+
const riskClass = entry.assessment.riskTier === 'low'
|
|
324
|
+
? 'ok'
|
|
325
|
+
: entry.assessment.riskTier === 'medium'
|
|
326
|
+
? 'warn'
|
|
327
|
+
: 'bad';
|
|
328
|
+
const searchKey = escapeHtml(`${entry.item.id} ${entry.item.name} ${entry.item.capabilities.join(' ')}`.toLowerCase());
|
|
329
|
+
const previewChips = entry.item.capabilities
|
|
330
|
+
.slice(0, 3)
|
|
331
|
+
.map((cap) => `<span class="chip">${escapeHtml(cap)}</span>`)
|
|
332
|
+
.join('');
|
|
333
|
+
const allChips = entry.item.capabilities.length > 0
|
|
334
|
+
? entry.item.capabilities.map((cap) => `<span class="chip">${escapeHtml(cap)}</span>`).join('')
|
|
335
|
+
: '<span class="chip">no capability tags</span>';
|
|
336
|
+
return `<article class="detail-card"
|
|
337
|
+
data-search="${searchKey}"
|
|
338
|
+
data-kind="${escapeHtml(entry.item.kind)}"
|
|
339
|
+
data-risk="${escapeHtml(entry.assessment.riskTier)}"
|
|
340
|
+
data-status="${escapeHtml(status)}"
|
|
341
|
+
data-name="${escapeHtml(entry.item.name.toLowerCase())}"
|
|
342
|
+
data-trust="${trustScore.toFixed(0)}"
|
|
343
|
+
data-riskscore="${entry.assessment.riskScore.toFixed(0)}">
|
|
344
|
+
<div class="card-header" onclick="toggleCard(this)">
|
|
345
|
+
<div class="card-title-row">
|
|
277
346
|
<h3 class="title">${escapeHtml(entry.item.name)}</h3>
|
|
278
|
-
<div class="meta mono">${escapeHtml(entry.item.id)}</div>
|
|
279
|
-
</div>
|
|
280
|
-
<div>
|
|
281
347
|
<span class="pill">${escapeHtml(entry.item.kind)}</span>
|
|
282
348
|
</div>
|
|
349
|
+
<div class="meta">${escapeHtml(entry.item.id)}</div>
|
|
350
|
+
<div class="pill-row">
|
|
351
|
+
<span class="pill">trust: ${trustScore.toFixed(0)}</span>
|
|
352
|
+
<span class="pill ${riskClass}">risk: ${escapeHtml(entry.assessment.riskTier)} (${entry.assessment.riskScore.toFixed(0)})</span>
|
|
353
|
+
<span class="pill ${statusClass}">${escapeHtml(status)}</span>
|
|
354
|
+
</div>
|
|
355
|
+
${previewChips ? `<div class="chips">${previewChips}</div>` : ''}
|
|
356
|
+
<div class="expand-hint">▼ click for details</div>
|
|
357
|
+
</div>
|
|
358
|
+
<div class="card-body">
|
|
359
|
+
<div class="line">${escapeHtml(entry.item.description)}</div>
|
|
360
|
+
${benefitSummary ? `<div class="line"><span class="label">What it does:</span> ${escapeHtml(benefitSummary)}</div>` : ''}
|
|
361
|
+
${bestFor.length > 0 ? `<div class="line"><span class="label">Best for:</span> ${escapeHtml(bestFor.join('; '))}</div>` : ''}
|
|
362
|
+
${tradeoffs.length > 0 ? `<div class="line"><span class="label">Tradeoffs:</span> ${escapeHtml(tradeoffs.join('; '))}</div>` : ''}
|
|
363
|
+
<div class="line"><span class="label">Capabilities:</span></div>
|
|
364
|
+
<div class="chips" style="margin-top:6px">${allChips}</div>
|
|
365
|
+
<div class="line"><span class="label">Decision:</span> trust ${trustScore.toFixed(0)}/100 (${escapeHtml(describeTrustBand(trustScore))}), risk ${entry.assessment.riskScore.toFixed(0)}/100 (${escapeHtml(entry.assessment.riskTier)}; ${escapeHtml(describeRiskBand(entry.assessment.riskScore, policy))}), status ${escapeHtml(status)}.</div>
|
|
366
|
+
<div class="line"><span class="label">Risk reasons:</span> ${escapeHtml(entry.assessment.reasons.join('; '))}</div>
|
|
367
|
+
<div class="line"><span class="label">Provenance:</span> provider=${escapeHtml(entry.item.provider)} source=${escapeHtml(entry.item.source)} confidence=${escapeHtml(confidence)} catalog=${escapeHtml(catalogType)}</div>
|
|
368
|
+
${sourceRepo ? `<div class="line"><span class="label">Source repo:</span> <a class="link" href="${escapeHtml(sourceRepo)}" target="_blank" rel="noopener">${escapeHtml(sourceRepo)}</a></div>` : ''}
|
|
369
|
+
${sourcePage ? `<div class="line"><span class="label">Source page:</span> <a class="link" href="${escapeHtml(sourcePage)}" target="_blank" rel="noopener">${escapeHtml(sourcePage)}</a></div>` : ''}
|
|
370
|
+
<pre class="mono">${escapeHtml(installHint)}</pre>
|
|
283
371
|
</div>
|
|
284
|
-
<div class="line">${escapeHtml(entry.item.description)}</div>
|
|
285
|
-
<div class="line"><span class="label">Decision:</span> trust ${trustScore.toFixed(0)}/100 (${escapeHtml(describeTrustBand(trustScore))}), risk ${entry.assessment.riskScore.toFixed(0)}/100 (${escapeHtml(entry.assessment.riskTier)}; ${escapeHtml(describeRiskBand(entry.assessment.riskScore, policy))}), status ${escapeHtml(status)}.</div>
|
|
286
|
-
<div class="line"><span class="label">Risk reasons:</span> ${escapeHtml(entry.assessment.reasons.join('; '))}</div>
|
|
287
|
-
<div class="line"><span class="label">Provenance:</span> provider=${escapeHtml(entry.item.provider)} source=${escapeHtml(entry.item.source)} confidence=${escapeHtml(confidence)} catalog=${escapeHtml(catalogType)}</div>
|
|
288
|
-
${sourceRepo
|
|
289
|
-
? `<div class="line"><span class="label">Source repo:</span> <a class="link" href="${escapeHtml(sourceRepo)}">${escapeHtml(sourceRepo)}</a></div>`
|
|
290
|
-
: ''}
|
|
291
|
-
${sourcePage
|
|
292
|
-
? `<div class="line"><span class="label">Source page:</span> <a class="link" href="${escapeHtml(sourcePage)}">${escapeHtml(sourcePage)}</a></div>`
|
|
293
|
-
: ''}
|
|
294
|
-
${bestFor.length > 0
|
|
295
|
-
? `<div class="line"><span class="label">Best for:</span> ${escapeHtml(bestFor.join('; '))}</div>`
|
|
296
|
-
: ''}
|
|
297
|
-
${tradeoffs.length > 0
|
|
298
|
-
? `<div class="line"><span class="label">Tradeoffs:</span> ${escapeHtml(tradeoffs.join('; '))}</div>`
|
|
299
|
-
: ''}
|
|
300
|
-
<div class="chips">${renderChips(entry.item.capabilities)}</div>
|
|
301
|
-
<pre class="mono">${escapeHtml(installHint)}</pre>
|
|
302
372
|
</article>`;
|
|
303
373
|
}
|
|
304
|
-
function renderChips(values) {
|
|
305
|
-
if (values.length === 0) {
|
|
306
|
-
return '<span class="chip">no capability tags</span>';
|
|
307
|
-
}
|
|
308
|
-
return values
|
|
309
|
-
.slice(0, 8)
|
|
310
|
-
.map((value) => `<span class="chip">${escapeHtml(value)}</span>`)
|
|
311
|
-
.join('');
|
|
312
|
-
}
|
|
313
374
|
function buildInstallHint(item) {
|
|
314
375
|
if (item.install.kind === 'manual') {
|
|
315
376
|
if (item.install.url) {
|
package/package.json
CHANGED