@shnitzel/plugscout 0.3.1
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/LICENSE +21 -0
- package/README.md +228 -0
- package/assets/cli/logo.txt +24 -0
- package/config/item-insights.json +316 -0
- package/config/providers.json +46 -0
- package/config/ranking-policy.json +14 -0
- package/config/recommendation-weights.json +7 -0
- package/config/registries.json +1423 -0
- package/config/security-policy.json +19 -0
- package/config/sources.json +30 -0
- package/data/catalog/items.json +182109 -0
- package/data/catalog/mcps.json +163843 -0
- package/data/catalog/skills.json +4768 -0
- package/data/catalog/sync-state.json +62 -0
- package/data/curated/mcps.json +78 -0
- package/data/curated/skills.json +174 -0
- package/data/quarantine/quarantined.json +3 -0
- package/data/raw/2024-05-15/mcps.json +20 -0
- package/data/raw/2024-05-20/skills.json +20 -0
- package/data/raw/2024-06-05/mcps.json +20 -0
- package/data/raw/2024-06-05/skills.json +29 -0
- package/data/security-reports/.gitkeep +0 -0
- package/data/security-reports/2026-02-06/report.json +8 -0
- package/data/security-reports/2026-02-10/report.json +9 -0
- package/data/security-reports/2026-02-11/report.json +9 -0
- package/data/security-reports/2026-02-12/report.json +9 -0
- package/data/security-reports/2026-02-13/report.json +8 -0
- package/data/security-reports/2026-02-14/report.json +8 -0
- package/data/security-reports/2026-02-23/report.json +8 -0
- package/data/security-reports/2026-02-25/report.json +8 -0
- package/data/security-reports/2026-02-26/report.json +8 -0
- package/data/security-reports/2026-03-10/report.json +8 -0
- package/data/security-reports/audits/.gitkeep +0 -0
- package/data/security-reports/audits/2026-02-06T10-17-33-872Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-06T10-17-33-881Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-10T20-22-24-474Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-10T20-22-24-483Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-10T20-42-12-305Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-10T20-42-12-319Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-10T20-43-15-728Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-10T20-43-15-738Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-10T21-22-14-047Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-10T21-22-14-051Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-10T21-29-59-237Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-10T21-29-59-243Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-11T20-21-51-074Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-11T20-21-51-123Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-11T20-28-33-021Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-11T20-28-33-026Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-11T20-34-43-623Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-11T20-34-43-625Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-11T21-06-33-281Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-11T21-06-33-285Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-11T21-08-58-836Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-11T21-08-58-843Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-12T12-26-07-150Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-12T12-26-07-159Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-12T14-37-36-565Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-12T14-37-36-569Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-12T14-47-32-103Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-12T14-47-32-213Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-12T14-47-47-769Z-mcp_filesystem.json +8 -0
- package/data/security-reports/audits/2026-02-12T15-05-49-085Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-12T15-05-49-087Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-12T16-37-42-204Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-12T16-37-42-243Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-12T16-47-16-589Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-12T16-47-16-596Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-12T17-38-24-899Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-12T17-38-24-905Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-12T17-56-00-835Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-12T17-56-00-840Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-12T18-19-26-005Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-12T18-19-26-008Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-12T18-34-38-642Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-12T18-34-38-645Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-13T05-44-27-648Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-13T05-44-27-656Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-13T05-48-50-827Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-13T05-48-50-900Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-13T10-53-33-850Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-13T10-53-33-853Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-14T17-51-27-279Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-14T17-51-27-282Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-14T19-43-39-991Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-14T19-43-39-997Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-23T19-24-43-515Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-23T19-24-43-518Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-25T14-45-02-763Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-25T14-45-02-778Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-25T14-46-58-957Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-25T14-46-58-960Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-25T14-57-37-133Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-25T14-57-37-139Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-25T15-03-23-507Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-25T15-03-23-513Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-25T15-03-41-157Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-25T15-03-41-162Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-25T15-05-18-042Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-25T15-05-18-048Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-25T15-39-08-519Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-25T15-39-08-526Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-25T18-35-54-463Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-25T18-35-54-466Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T05-52-21-092Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T05-52-21-093Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T05-52-27-076Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T05-52-27-079Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T05-52-27-084Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T05-52-27-086Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T05-52-37-249Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T05-52-37-258Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T05-52-37-259Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T05-52-37-274Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T05-53-28-389Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T05-53-28-391Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T05-53-33-868Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T05-53-33-880Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T05-53-33-892Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T05-53-33-900Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T05-53-43-064Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T05-53-43-066Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T05-53-43-068Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T14-55-47-466Z-claude-plugin_workspace-ops.json +8 -0
- package/data/security-reports/audits/2026-02-26T14-55-47-468Z-copilot-extension_repo-security.json +8 -0
- package/data/security-reports/audits/2026-02-26T16-55-59-431Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T16-55-59-432Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T16-55-59-435Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T16-55-59-439Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T16-56-08-566Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T16-56-08-570Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T16-56-08-589Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T16-56-08-591Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T16-56-47-356Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T16-56-47-358Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T16-56-53-607Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T16-56-53-612Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T16-56-53-624Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T16-56-53-628Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T16-57-09-879Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T16-57-09-881Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T16-57-10-846Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-02-26T16-57-10-848Z-mcp_remote-browser.json +8 -0
- package/data/security-reports/audits/2026-03-10T18-15-05-007Z-claude-plugin_playwright.json +8 -0
- package/data/security-reports/audits/2026-03-10T18-36-16-092Z-claude-plugin_playwright.json +8 -0
- package/data/whitelist/approved.json +5 -0
- package/dist/catalog/adapter.js +39 -0
- package/dist/catalog/adapters/claude-code-marketplace-v1.js +260 -0
- package/dist/catalog/adapters/claude-connectors-scrape-v1.js +107 -0
- package/dist/catalog/adapters/claude-plugins-scrape-v1.js +107 -0
- package/dist/catalog/adapters/claude-plugins-v0.1.js +48 -0
- package/dist/catalog/adapters/copilot-extensions-v0.1.js +48 -0
- package/dist/catalog/adapters/copilot-plugin-marketplace-v1.js +117 -0
- package/dist/catalog/adapters/mcp-registry-v0.1.js +211 -0
- package/dist/catalog/adapters/openai-skills-github-v1.js +100 -0
- package/dist/catalog/adapters/openai-skills-v1.js +48 -0
- package/dist/catalog/adapters/shared.js +94 -0
- package/dist/catalog/remote-registry.js +196 -0
- package/dist/catalog/repository.js +161 -0
- package/dist/catalog/sync-state.js +61 -0
- package/dist/catalog/sync.js +153 -0
- package/dist/cli.js +25 -0
- package/dist/commands/ExplainerVideo.js +225 -0
- package/dist/commands/ingest.js +11 -0
- package/dist/commands/validate-data.js +10 -0
- package/dist/config/runtime.js +51 -0
- package/dist/config/sources.js +21 -0
- package/dist/ingestion/mcps.js +77 -0
- package/dist/ingestion/skills.js +76 -0
- package/dist/install/dependencies.js +58 -0
- package/dist/install/review-state.js +70 -0
- package/dist/install/skillsh.js +245 -0
- package/dist/interfaces/cli/doctor.js +90 -0
- package/dist/interfaces/cli/formatters/colors.js +24 -0
- package/dist/interfaces/cli/formatters/csv.js +10 -0
- package/dist/interfaces/cli/formatters/json.js +3 -0
- package/dist/interfaces/cli/formatters/markdown.js +6 -0
- package/dist/interfaces/cli/formatters/table.js +82 -0
- package/dist/interfaces/cli/index.js +1277 -0
- package/dist/interfaces/cli/options.js +93 -0
- package/dist/interfaces/cli/output.js +9 -0
- package/dist/interfaces/cli/types.js +1 -0
- package/dist/interfaces/cli/ui/home.js +114 -0
- package/dist/interfaces/cli/ui/web-report.js +384 -0
- package/dist/interfaces/cli/update-check.js +180 -0
- package/dist/lib/json.js +11 -0
- package/dist/lib/logger.js +13 -0
- package/dist/lib/paths.js +18 -0
- package/dist/lib/validation/contracts.js +245 -0
- package/dist/mcps/normalize.js +38 -0
- package/dist/models/records.js +31 -0
- package/dist/recommendation/engine.js +135 -0
- package/dist/recommendation/project-analysis.js +231 -0
- package/dist/recommendation/requirements.js +58 -0
- package/dist/security/assessment.js +56 -0
- package/dist/security/whitelist.js +70 -0
- package/dist/skills/normalize.js +39 -0
- package/dist/validation/curated.js +72 -0
- package/dist/video/Root.js +6 -0
- package/dist/video/index.js +3 -0
- package/package.json +102 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { CatalogKindSchema } from '../../lib/validation/contracts.js';
|
|
2
|
+
const KIND_ALIASES = {
|
|
3
|
+
skill: 'skill',
|
|
4
|
+
skills: 'skill',
|
|
5
|
+
mcp: 'mcp',
|
|
6
|
+
mcps: 'mcp',
|
|
7
|
+
server: 'mcp',
|
|
8
|
+
servers: 'mcp',
|
|
9
|
+
'claude-plugin': 'claude-plugin',
|
|
10
|
+
'claude-plugins': 'claude-plugin',
|
|
11
|
+
'claude plugin': 'claude-plugin',
|
|
12
|
+
'claude plugins': 'claude-plugin',
|
|
13
|
+
plugin: 'claude-plugin',
|
|
14
|
+
plugins: 'claude-plugin',
|
|
15
|
+
'claude-connector': 'claude-connector',
|
|
16
|
+
'claude-connectors': 'claude-connector',
|
|
17
|
+
'claude connector': 'claude-connector',
|
|
18
|
+
'claude connectors': 'claude-connector',
|
|
19
|
+
connector: 'claude-connector',
|
|
20
|
+
connectors: 'claude-connector',
|
|
21
|
+
'copilot-extension': 'copilot-extension',
|
|
22
|
+
'copilot-extensions': 'copilot-extension',
|
|
23
|
+
'copilot extension': 'copilot-extension',
|
|
24
|
+
'copilot extensions': 'copilot-extension',
|
|
25
|
+
extension: 'copilot-extension',
|
|
26
|
+
extensions: 'copilot-extension',
|
|
27
|
+
copilot: 'copilot-extension'
|
|
28
|
+
};
|
|
29
|
+
export function readFlag(args, flag) {
|
|
30
|
+
const index = args.indexOf(flag);
|
|
31
|
+
if (index === -1) {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
return args[index + 1];
|
|
35
|
+
}
|
|
36
|
+
export function hasFlag(args, flag) {
|
|
37
|
+
return args.includes(flag);
|
|
38
|
+
}
|
|
39
|
+
export function readKinds(args) {
|
|
40
|
+
const value = readFlag(args, '--kind');
|
|
41
|
+
if (!value) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
return value
|
|
45
|
+
.split(',')
|
|
46
|
+
.map((segment) => segment.trim())
|
|
47
|
+
.filter((segment) => segment.length > 0)
|
|
48
|
+
.map((kind) => normalizeKind(kind));
|
|
49
|
+
}
|
|
50
|
+
export function normalizeKind(raw) {
|
|
51
|
+
const normalized = raw.trim().toLowerCase();
|
|
52
|
+
const alias = KIND_ALIASES[normalized];
|
|
53
|
+
if (alias) {
|
|
54
|
+
return alias;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
return CatalogKindSchema.parse(normalized);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
throw new Error(`Invalid --kind value: ${raw}. Expected one of: skill, mcp, claude-plugin, claude-connector, copilot-extension. Aliases also supported: skills, mcps, plugins, connectors, extensions.`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export function readCsvList(args, flag) {
|
|
64
|
+
const value = readFlag(args, flag);
|
|
65
|
+
if (!value) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
return value
|
|
69
|
+
.split(',')
|
|
70
|
+
.map((part) => part.trim())
|
|
71
|
+
.filter((part) => part.length > 0);
|
|
72
|
+
}
|
|
73
|
+
export function readLimit(args, defaultValue) {
|
|
74
|
+
const value = readFlag(args, '--limit');
|
|
75
|
+
if (!value) {
|
|
76
|
+
return defaultValue;
|
|
77
|
+
}
|
|
78
|
+
const parsed = Number(value);
|
|
79
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
80
|
+
throw new Error(`Invalid --limit value: ${value}`);
|
|
81
|
+
}
|
|
82
|
+
return parsed;
|
|
83
|
+
}
|
|
84
|
+
export function readSort(args, defaultValue = 'score') {
|
|
85
|
+
const value = readFlag(args, '--sort');
|
|
86
|
+
if (!value) {
|
|
87
|
+
return defaultValue;
|
|
88
|
+
}
|
|
89
|
+
if (value === 'score' || value === 'trust' || value === 'risk' || value === 'fit' || value === 'name') {
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
throw new Error(`Invalid --sort value: ${value}. Expected one of: score, trust, risk, fit, name`);
|
|
93
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import { loadCatalogItems, loadQuarantine, loadWhitelist } from '../../../catalog/repository.js';
|
|
3
|
+
import { getStaleRegistries, loadSyncState } from '../../../catalog/sync-state.js';
|
|
4
|
+
import { getPackagePath } from '../../../lib/paths.js';
|
|
5
|
+
import { colors } from '../formatters/colors.js';
|
|
6
|
+
export async function renderHomeScreen() {
|
|
7
|
+
const [logo, pkg, catalogStats, runtimeStats] = await Promise.all([
|
|
8
|
+
readLogo(),
|
|
9
|
+
readPackageMeta(),
|
|
10
|
+
readCatalogStats(),
|
|
11
|
+
readRuntimeStats()
|
|
12
|
+
]);
|
|
13
|
+
const lines = [];
|
|
14
|
+
const version = pkg.version ?? '0.0.0';
|
|
15
|
+
const author = pkg.author ?? '';
|
|
16
|
+
const renderedLogo = logo
|
|
17
|
+
.replace('{{version}}', `v${version}`)
|
|
18
|
+
.replace('{{author}}', author || 'unknown');
|
|
19
|
+
lines.push(colorIfTty(renderedLogo.trimEnd(), colors.cyan));
|
|
20
|
+
lines.push('');
|
|
21
|
+
lines.push('Discover and safely install Claude plugins, Claude connectors, Copilot extensions, Skills, and MCP servers.');
|
|
22
|
+
lines.push('');
|
|
23
|
+
lines.push(colorIfTty('Catalog', colors.bold));
|
|
24
|
+
lines.push(`- items=${catalogStats.items} skill=${catalogStats.skill} mcp=${catalogStats.mcp} claude-plugin=${catalogStats.claudePlugin} claude-connector=${catalogStats.claudeConnector} copilot-extension=${catalogStats.copilotExtension}`);
|
|
25
|
+
lines.push(`- stale-registries=${runtimeStats.staleRegistries} whitelist=${runtimeStats.whitelist} quarantined=${runtimeStats.quarantined}`);
|
|
26
|
+
lines.push('');
|
|
27
|
+
lines.push(colorIfTty('Quick actions', colors.bold));
|
|
28
|
+
lines.push('- plugscout doctor');
|
|
29
|
+
lines.push('- plugscout status --verbose');
|
|
30
|
+
lines.push('- plugscout recommend --project . --only-safe --limit 10');
|
|
31
|
+
lines.push('- plugscout sync --dry-run');
|
|
32
|
+
lines.push('- plugscout help');
|
|
33
|
+
lines.push('');
|
|
34
|
+
lines.push(colorIfTty('Examples', colors.bold));
|
|
35
|
+
lines.push('- plugscout list --kind connectors --limit 10');
|
|
36
|
+
lines.push('- plugscout search github');
|
|
37
|
+
lines.push('- plugscout show --id claude-connector:asana');
|
|
38
|
+
lines.push('');
|
|
39
|
+
lines.push(colorIfTty('Kind aliases', colors.bold));
|
|
40
|
+
lines.push('- skills, mcps, plugins, connectors, extensions');
|
|
41
|
+
lines.push('');
|
|
42
|
+
lines.push(colorIfTty('Ranking meaning', colors.bold));
|
|
43
|
+
lines.push('- `top` and `recommend` are repo-aware suggestions, not global popularity charts.');
|
|
44
|
+
lines.push('- score = fit + trust + freshness - security - blocked');
|
|
45
|
+
lines.push('- higher score means a better match for this repo under current policy');
|
|
46
|
+
lines.push('- review each suggestion before installing; do not install blindly from rank alone');
|
|
47
|
+
return lines.join('\n');
|
|
48
|
+
}
|
|
49
|
+
async function readLogo() {
|
|
50
|
+
try {
|
|
51
|
+
return await fs.readFile(getPackagePath('assets/cli/logo.txt'), 'utf8');
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return 'PlugScout';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async function readPackageMeta() {
|
|
58
|
+
try {
|
|
59
|
+
const raw = await fs.readFile(getPackagePath('package.json'), 'utf8');
|
|
60
|
+
return JSON.parse(raw);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return { name: 'plugscout', version: '0.0.0' };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async function readCatalogStats() {
|
|
67
|
+
const items = await loadCatalogItems();
|
|
68
|
+
let skill = 0;
|
|
69
|
+
let mcp = 0;
|
|
70
|
+
let claudePlugin = 0;
|
|
71
|
+
let claudeConnector = 0;
|
|
72
|
+
let copilotExtension = 0;
|
|
73
|
+
items.forEach((item) => {
|
|
74
|
+
if (item.kind === 'skill') {
|
|
75
|
+
skill += 1;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (item.kind === 'mcp') {
|
|
79
|
+
mcp += 1;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (item.kind === 'claude-plugin') {
|
|
83
|
+
claudePlugin += 1;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (item.kind === 'claude-connector') {
|
|
87
|
+
claudeConnector += 1;
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
copilotExtension += 1;
|
|
91
|
+
});
|
|
92
|
+
return {
|
|
93
|
+
items: items.length,
|
|
94
|
+
skill,
|
|
95
|
+
mcp,
|
|
96
|
+
claudePlugin,
|
|
97
|
+
claudeConnector,
|
|
98
|
+
copilotExtension
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
async function readRuntimeStats() {
|
|
102
|
+
const [syncState, whitelist, quarantine] = await Promise.all([loadSyncState(), loadWhitelist(), loadQuarantine()]);
|
|
103
|
+
return {
|
|
104
|
+
staleRegistries: getStaleRegistries(syncState).length,
|
|
105
|
+
whitelist: whitelist.size,
|
|
106
|
+
quarantined: quarantine.length
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function colorIfTty(value, apply) {
|
|
110
|
+
if (!process.stdout.isTTY || process.env.NO_COLOR === '1') {
|
|
111
|
+
return value;
|
|
112
|
+
}
|
|
113
|
+
return apply(value);
|
|
114
|
+
}
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { loadCatalogItems, loadQuarantine, loadWhitelist } from '../../../catalog/repository.js';
|
|
4
|
+
import { loadItemInsights, loadSecurityPolicy } from '../../../config/runtime.js';
|
|
5
|
+
import { buildAssessment } from '../../../security/assessment.js';
|
|
6
|
+
export async function writeWebReport(options) {
|
|
7
|
+
const [items, whitelist, quarantine, policy, insights] = await Promise.all([
|
|
8
|
+
loadCatalogItems(),
|
|
9
|
+
loadWhitelist(),
|
|
10
|
+
loadQuarantine(),
|
|
11
|
+
loadSecurityPolicy(),
|
|
12
|
+
loadItemInsights()
|
|
13
|
+
]);
|
|
14
|
+
const filtered = filterByKinds(items, options.kinds).slice(0, options.limit);
|
|
15
|
+
const quarantineIds = new Set(quarantine.map((entry) => entry.id));
|
|
16
|
+
const rows = filtered.map((item) => {
|
|
17
|
+
const assessment = buildAssessment(item, policy);
|
|
18
|
+
const blockedByPolicy = assessment.riskTier === 'high' || assessment.riskTier === 'critical';
|
|
19
|
+
const blocked = blockedByPolicy || quarantineIds.has(item.id);
|
|
20
|
+
return { item, assessment, blocked, approved: whitelist.has(item.id), insight: insights.get(item.id) };
|
|
21
|
+
});
|
|
22
|
+
const html = renderHtml(rows, {
|
|
23
|
+
totalItems: filtered.length,
|
|
24
|
+
whitelist: whitelist.size,
|
|
25
|
+
quarantined: quarantine.length
|
|
26
|
+
}, policy);
|
|
27
|
+
const resolvedOutput = path.resolve(options.outputPath);
|
|
28
|
+
await fs.mkdir(path.dirname(resolvedOutput), { recursive: true });
|
|
29
|
+
await fs.writeFile(resolvedOutput, html, 'utf8');
|
|
30
|
+
return { outputPath: resolvedOutput, items: filtered.length };
|
|
31
|
+
}
|
|
32
|
+
function filterByKinds(items, kinds) {
|
|
33
|
+
if (!kinds || kinds.length === 0) {
|
|
34
|
+
return items;
|
|
35
|
+
}
|
|
36
|
+
const set = new Set(kinds);
|
|
37
|
+
return items.filter((item) => set.has(item.kind));
|
|
38
|
+
}
|
|
39
|
+
function renderHtml(rows, stats, policy) {
|
|
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
|
+
const riskScale = escapeHtml(formatRiskScale(policy));
|
|
47
|
+
return `<!doctype html>
|
|
48
|
+
<html lang="en">
|
|
49
|
+
<head>
|
|
50
|
+
<meta charset="utf-8" />
|
|
51
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
52
|
+
<title>PlugScout Web Report</title>
|
|
53
|
+
<style>
|
|
54
|
+
:root {
|
|
55
|
+
color-scheme: dark;
|
|
56
|
+
--bg: #050916;
|
|
57
|
+
--panel: #0e1628;
|
|
58
|
+
--line: #22314d;
|
|
59
|
+
--text: #e5edf8;
|
|
60
|
+
--muted: #a8b6cc;
|
|
61
|
+
--ok: #22c55e;
|
|
62
|
+
--warn: #f59e0b;
|
|
63
|
+
--bad: #ef4444;
|
|
64
|
+
--accent: #60a5fa;
|
|
65
|
+
}
|
|
66
|
+
* { box-sizing: border-box; }
|
|
67
|
+
body {
|
|
68
|
+
margin: 0;
|
|
69
|
+
font: 15px/1.45 -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
70
|
+
color: var(--text);
|
|
71
|
+
background: radial-gradient(circle at top, #111b33 0%, var(--bg) 45%);
|
|
72
|
+
padding: 28px;
|
|
73
|
+
}
|
|
74
|
+
.wrap { max-width: 1460px; margin: 0 auto; }
|
|
75
|
+
h1 { margin: 0 0 8px; font-size: 34px; }
|
|
76
|
+
.sub { color: var(--muted); margin: 0 0 22px; }
|
|
77
|
+
.cards {
|
|
78
|
+
display: grid;
|
|
79
|
+
grid-template-columns: repeat(3, minmax(180px, 1fr));
|
|
80
|
+
gap: 12px;
|
|
81
|
+
margin-bottom: 18px;
|
|
82
|
+
}
|
|
83
|
+
.card {
|
|
84
|
+
background: var(--panel);
|
|
85
|
+
border: 1px solid var(--line);
|
|
86
|
+
border-radius: 12px;
|
|
87
|
+
padding: 14px;
|
|
88
|
+
}
|
|
89
|
+
.k { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: 0.05em; }
|
|
90
|
+
.v { font-size: 26px; font-weight: 700; margin-top: 4px; }
|
|
91
|
+
.section {
|
|
92
|
+
margin-top: 18px;
|
|
93
|
+
background: var(--panel);
|
|
94
|
+
border: 1px solid var(--line);
|
|
95
|
+
border-radius: 12px;
|
|
96
|
+
overflow: hidden;
|
|
97
|
+
}
|
|
98
|
+
.section-body { padding: 14px 16px; }
|
|
99
|
+
.section h2 {
|
|
100
|
+
margin: 0;
|
|
101
|
+
padding: 14px 16px;
|
|
102
|
+
font-size: 18px;
|
|
103
|
+
border-bottom: 1px solid var(--line);
|
|
104
|
+
color: var(--accent);
|
|
105
|
+
}
|
|
106
|
+
table { width: 100%; border-collapse: collapse; }
|
|
107
|
+
th, td {
|
|
108
|
+
text-align: left;
|
|
109
|
+
padding: 10px 12px;
|
|
110
|
+
border-bottom: 1px solid #1d2a41;
|
|
111
|
+
vertical-align: top;
|
|
112
|
+
word-break: break-word;
|
|
113
|
+
}
|
|
114
|
+
th { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: 0.04em; }
|
|
115
|
+
.pill {
|
|
116
|
+
display: inline-block;
|
|
117
|
+
border-radius: 999px;
|
|
118
|
+
border: 1px solid #314664;
|
|
119
|
+
padding: 2px 9px;
|
|
120
|
+
font-size: 12px;
|
|
121
|
+
color: var(--text);
|
|
122
|
+
white-space: nowrap;
|
|
123
|
+
}
|
|
124
|
+
.ok { color: var(--ok); border-color: #166534; }
|
|
125
|
+
.warn { color: var(--warn); border-color: #854d0e; }
|
|
126
|
+
.bad { color: var(--bad); border-color: #7f1d1d; }
|
|
127
|
+
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 13px; }
|
|
128
|
+
.detail-grid {
|
|
129
|
+
display: grid;
|
|
130
|
+
grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
|
|
131
|
+
gap: 12px;
|
|
132
|
+
padding: 14px;
|
|
133
|
+
}
|
|
134
|
+
.detail-card {
|
|
135
|
+
border: 1px solid #243654;
|
|
136
|
+
border-radius: 10px;
|
|
137
|
+
background: #081121;
|
|
138
|
+
padding: 12px;
|
|
139
|
+
}
|
|
140
|
+
.detail-head {
|
|
141
|
+
display: flex;
|
|
142
|
+
align-items: flex-start;
|
|
143
|
+
justify-content: space-between;
|
|
144
|
+
gap: 10px;
|
|
145
|
+
margin-bottom: 8px;
|
|
146
|
+
}
|
|
147
|
+
.title {
|
|
148
|
+
margin: 0;
|
|
149
|
+
font-size: 16px;
|
|
150
|
+
line-height: 1.3;
|
|
151
|
+
}
|
|
152
|
+
.meta {
|
|
153
|
+
color: var(--muted);
|
|
154
|
+
font-size: 12px;
|
|
155
|
+
margin-top: 4px;
|
|
156
|
+
}
|
|
157
|
+
.line { margin-top: 8px; color: var(--text); }
|
|
158
|
+
.line .label { color: var(--muted); }
|
|
159
|
+
.chips { margin-top: 8px; display: flex; flex-wrap: wrap; gap: 6px; }
|
|
160
|
+
.chip {
|
|
161
|
+
border: 1px solid #2f476b;
|
|
162
|
+
border-radius: 999px;
|
|
163
|
+
padding: 2px 8px;
|
|
164
|
+
color: #c9dbf5;
|
|
165
|
+
font-size: 12px;
|
|
166
|
+
}
|
|
167
|
+
.link { color: #93c5fd; text-decoration: none; }
|
|
168
|
+
.link:hover { text-decoration: underline; }
|
|
169
|
+
pre {
|
|
170
|
+
margin: 8px 0 0;
|
|
171
|
+
padding: 9px 10px;
|
|
172
|
+
border: 1px solid #243654;
|
|
173
|
+
border-radius: 8px;
|
|
174
|
+
background: #060f1d;
|
|
175
|
+
overflow-x: auto;
|
|
176
|
+
color: #dbeafe;
|
|
177
|
+
}
|
|
178
|
+
</style>
|
|
179
|
+
</head>
|
|
180
|
+
<body>
|
|
181
|
+
<div class="wrap">
|
|
182
|
+
<h1>PlugScout Web Report</h1>
|
|
183
|
+
<p class="sub">Readable catalog view for Claude plugins, Claude connectors, Copilot extensions, Skills, and MCP servers.</p>
|
|
184
|
+
<div class="cards">
|
|
185
|
+
<div class="card"><div class="k">Items</div><div class="v">${stats.totalItems}</div></div>
|
|
186
|
+
<div class="card"><div class="k">Claude Plugins</div><div class="v">${kindCounts['claude-plugin']}</div></div>
|
|
187
|
+
<div class="card"><div class="k">Claude Connectors</div><div class="v">${kindCounts['claude-connector']}</div></div>
|
|
188
|
+
<div class="card"><div class="k">Copilot Extensions</div><div class="v">${kindCounts['copilot-extension']}</div></div>
|
|
189
|
+
<div class="card"><div class="k">Skills</div><div class="v">${kindCounts.skill}</div></div>
|
|
190
|
+
<div class="card"><div class="k">MCP Servers</div><div class="v">${kindCounts.mcp}</div></div>
|
|
191
|
+
<div class="card"><div class="k">Whitelist / Quarantine</div><div class="v">${stats.whitelist} / ${stats.quarantined}</div></div>
|
|
192
|
+
</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
|
+
</div>
|
|
207
|
+
</body>
|
|
208
|
+
</html>`;
|
|
209
|
+
}
|
|
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
|
+
function renderDetailCard(entry, policy) {
|
|
264
|
+
const metadata = asMetadata(entry.item.metadata);
|
|
265
|
+
const trustScore = computeTrustScore(entry.item);
|
|
266
|
+
const confidence = stringOr(metadata.sourceConfidence, 'official');
|
|
267
|
+
const catalogType = stringOr(metadata.catalogType, 'standard');
|
|
268
|
+
const sourceRepo = typeof metadata.sourceRepo === 'string' ? metadata.sourceRepo : '';
|
|
269
|
+
const sourcePage = typeof metadata.sourcePage === 'string' ? metadata.sourcePage : '';
|
|
270
|
+
const status = entry.blocked ? 'blocked' : entry.approved ? 'approved' : 'allowed';
|
|
271
|
+
const installHint = buildInstallHint(entry.item);
|
|
272
|
+
const bestFor = entry.insight?.bestFor ?? [];
|
|
273
|
+
const tradeoffs = entry.insight?.tradeoffs ?? [];
|
|
274
|
+
return `<article class="detail-card">
|
|
275
|
+
<div class="detail-head">
|
|
276
|
+
<div>
|
|
277
|
+
<h3 class="title">${escapeHtml(entry.item.name)}</h3>
|
|
278
|
+
<div class="meta mono">${escapeHtml(entry.item.id)}</div>
|
|
279
|
+
</div>
|
|
280
|
+
<div>
|
|
281
|
+
<span class="pill">${escapeHtml(entry.item.kind)}</span>
|
|
282
|
+
</div>
|
|
283
|
+
</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
|
+
</article>`;
|
|
303
|
+
}
|
|
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
|
+
function buildInstallHint(item) {
|
|
314
|
+
if (item.install.kind === 'manual') {
|
|
315
|
+
if (item.install.url) {
|
|
316
|
+
return `Manual install: ${item.install.url}`;
|
|
317
|
+
}
|
|
318
|
+
return `Manual install: ${item.install.instructions}`;
|
|
319
|
+
}
|
|
320
|
+
const args = item.install.args.length > 0 ? ` ${item.install.args.join(' ')}` : '';
|
|
321
|
+
return `${item.install.kind} ${item.install.target}${args}`;
|
|
322
|
+
}
|
|
323
|
+
function computeTrustScore(item) {
|
|
324
|
+
return (item.maintenanceSignal + item.provenanceSignal + item.adoptionSignal) / 3;
|
|
325
|
+
}
|
|
326
|
+
function describeTrustBand(score) {
|
|
327
|
+
if (score >= 80) {
|
|
328
|
+
return 'high confidence';
|
|
329
|
+
}
|
|
330
|
+
if (score >= 60) {
|
|
331
|
+
return 'moderate confidence';
|
|
332
|
+
}
|
|
333
|
+
return 'needs review';
|
|
334
|
+
}
|
|
335
|
+
function describeRiskBand(score, policy) {
|
|
336
|
+
if (score <= policy.thresholds.lowMax) {
|
|
337
|
+
return 'low-risk zone';
|
|
338
|
+
}
|
|
339
|
+
if (score <= policy.thresholds.mediumMax) {
|
|
340
|
+
return 'medium-risk zone';
|
|
341
|
+
}
|
|
342
|
+
if (score <= policy.thresholds.highMax) {
|
|
343
|
+
return 'high-risk zone';
|
|
344
|
+
}
|
|
345
|
+
return 'critical-risk zone';
|
|
346
|
+
}
|
|
347
|
+
function formatRiskScale(policy) {
|
|
348
|
+
const low = policy.thresholds.lowMax;
|
|
349
|
+
const medium = policy.thresholds.mediumMax;
|
|
350
|
+
const high = policy.thresholds.highMax;
|
|
351
|
+
return `low 0-${low}, medium ${low + 1}-${medium}, high ${medium + 1}-${high}, critical ${high + 1}-${policy.thresholds.criticalMax}; install gate blocks: ${policy.installGate.blockTiers.join(', ')}.`;
|
|
352
|
+
}
|
|
353
|
+
function asMetadata(value) {
|
|
354
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
355
|
+
return {};
|
|
356
|
+
}
|
|
357
|
+
return value;
|
|
358
|
+
}
|
|
359
|
+
function stringOr(value, fallback) {
|
|
360
|
+
if (typeof value === 'string' && value.trim().length > 0) {
|
|
361
|
+
return value;
|
|
362
|
+
}
|
|
363
|
+
return fallback;
|
|
364
|
+
}
|
|
365
|
+
function escapeHtml(value) {
|
|
366
|
+
return value
|
|
367
|
+
.replaceAll('&', '&')
|
|
368
|
+
.replaceAll('<', '<')
|
|
369
|
+
.replaceAll('>', '>')
|
|
370
|
+
.replaceAll('"', '"')
|
|
371
|
+
.replaceAll("'", ''');
|
|
372
|
+
}
|
|
373
|
+
function countByKind(items) {
|
|
374
|
+
return items.reduce((acc, item) => {
|
|
375
|
+
acc[item.kind] += 1;
|
|
376
|
+
return acc;
|
|
377
|
+
}, {
|
|
378
|
+
skill: 0,
|
|
379
|
+
mcp: 0,
|
|
380
|
+
'claude-plugin': 0,
|
|
381
|
+
'claude-connector': 0,
|
|
382
|
+
'copilot-extension': 0
|
|
383
|
+
});
|
|
384
|
+
}
|