@stolyarovmn/dsh-ui-registry-aggregator 0.4.14 → 0.4.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -7
- package/lib/client.js +256 -39
- package/lib/core.js +130 -11
- package/lib/index.js +10 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,14 +29,20 @@ Search across connected sources, filter results, inspect evidence, and install a
|
|
|
29
29
|
- **Fallback command** — installable results also expose their `dsh plugin add …` command.
|
|
30
30
|
- **Responsive popularity metadata** — missing GitHub star counts are hydrated after Browse renders, so GitHub metadata cannot block the initial search result.
|
|
31
31
|
|
|
32
|
-
## What's new in 0.4.
|
|
32
|
+
## What's new in 0.4.15
|
|
33
33
|
|
|
34
|
-
0.4.
|
|
34
|
+
0.4.15 focuses on reliability and native DSH integration while preserving the existing Browse evidence path.
|
|
35
35
|
|
|
36
|
-
-
|
|
37
|
-
- npm
|
|
38
|
-
-
|
|
39
|
-
-
|
|
36
|
+
- lightweight source health checks are separated from verified DSH bundle counts;
|
|
37
|
+
- npm and GitHub counts load independently with native DSH progress indicators;
|
|
38
|
+
- GitHub requests retry transient failures, while health no longer consumes Search API quota;
|
|
39
|
+
- installed bundles are detected through the native Plugin Manager and duplicate installs are disabled;
|
|
40
|
+
- newer discovered versions are shown as advisory `Update available` notices;
|
|
41
|
+
- active installs can be cancelled through the native Plugin Manager request id;
|
|
42
|
+
- npm results can be enriched with exact GitHub provenance without broad GitHub pagination;
|
|
43
|
+
- search relevance is presentation-only, so GitHub stars, downloads, evidence, pagination, and explicit sorting keep their existing behavior.
|
|
44
|
+
|
|
45
|
+
Tested with DSH `0.1.7-rc.2`.
|
|
40
46
|
|
|
41
47
|
## Architecture
|
|
42
48
|
|
|
@@ -167,7 +173,7 @@ The order in which criteria are enabled defines priority.
|
|
|
167
173
|
## Compatibility
|
|
168
174
|
|
|
169
175
|
- DSH: `>=0.1.7-rc.1 <0.2.0`
|
|
170
|
-
- Tested with DSH `0.1.7-rc.
|
|
176
|
+
- Tested with DSH `0.1.7-rc.2`
|
|
171
177
|
|
|
172
178
|
Exact package compatibility is read from package metadata when a publisher declares it. Installation is validated again by the native DSH Plugin Manager.
|
|
173
179
|
|
package/lib/client.js
CHANGED
|
@@ -12,7 +12,7 @@ window.__ModuleLoader__.load({
|
|
|
12
12
|
IconCopyOutlineRegular, IconShareOutlineRegular, IconTrashOutlineRegular,
|
|
13
13
|
IconDatabaseOutlineRegular, IconCordisPluginOutlineRegular, IconLinkOutlineRegular,
|
|
14
14
|
IconWarningOutlineRegular, IconSearchOutlineRegular, IconClockOutlineRegular,
|
|
15
|
-
IconDownloadOutlineRegular, IconCheckOutlineRegular, IconChevronUpOutlineRegular,
|
|
15
|
+
IconDownloadOutlineRegular, IconCheckOutlineRegular, IconCloseOutlineRegular, IconChevronUpOutlineRegular,
|
|
16
16
|
IconChevronLeftOutlineRegular, IconChevronRightOutlineRegular, IconChevronsUpDownOutlineRegular,
|
|
17
17
|
IconFlatListOutlineRegular, IconShieldOutlineRegular, IconChecklistOutlineRegular, IconCodeOutlineRegular,
|
|
18
18
|
} = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
@@ -332,12 +332,16 @@ window.__ModuleLoader__.load({
|
|
|
332
332
|
|
|
333
333
|
.pm-card{padding:11px 12px}
|
|
334
334
|
.pm-card-top{display:flex;align-items:flex-start;justify-content:space-between;gap:10px}
|
|
335
|
+
.pm-card-install-actions{display:flex;align-items:center;gap:6px;flex:0 0 auto}
|
|
335
336
|
.pm-card-install{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;flex:0 0 30px;width:30px;height:30px;padding:0;border:.5px solid var(--dsw-alias-border-l3);border-radius:8px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);cursor:pointer}
|
|
336
337
|
.pm-card-install:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-brand-primary)}
|
|
337
338
|
.pm-card-install[data-state=installed]{color:var(--dsw-alias-state-success-primary)}
|
|
338
339
|
.pm-card-install[data-state=failed]{color:var(--dsw-alias-state-error-primary)}
|
|
339
340
|
.pm-card-install:disabled{opacity:.55;cursor:default}
|
|
340
341
|
.pm-card-install svg{display:block;width:16px;height:16px}
|
|
342
|
+
.pm-card-cancel{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;flex:0 0 30px;width:30px;height:30px;padding:0;border:.5px solid color-mix(in srgb,var(--pm-danger) 45%,var(--dsw-alias-border-l3));border-radius:8px;background:var(--dsw-alias-bg-layer-1);color:var(--pm-danger);cursor:pointer}
|
|
343
|
+
.pm-card-cancel:hover{background:color-mix(in srgb,var(--pm-danger) 9%,var(--dsw-alias-bg-layer-1));border-color:var(--pm-danger)}
|
|
344
|
+
.pm-card-cancel svg{display:block;width:14px;height:14px}
|
|
341
345
|
|
|
342
346
|
.pm-card-description{position:relative;margin-top:7px;min-width:0}
|
|
343
347
|
.pm-desc{margin:0;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:18px}
|
|
@@ -458,16 +462,16 @@ window.__ModuleLoader__.load({
|
|
|
458
462
|
const en = {
|
|
459
463
|
nav: "Registry Aggregator", kicker: "Federated plugin registry", title: "Registry Aggregator", lead: "Aggregate connected plugin registries and catalogs, search them as one index, and install through the native DSH Plugin Manager.",
|
|
460
464
|
"tab.sources": "Sources", "tab.browse": "Browse", "sources.title": "Connected sources", "sources.add": "Add source", "sources.addTitle": "Add a new source", "sources.addLead": "Connect a package registry to discover and install plugins.", "sources.manage": "Manage your connected registries and discover available plugins.", "sources.empty": "No plugin sources connected", "sources.emptyHint": "Add a catalog, npm registry, or GitHub source to begin.",
|
|
461
|
-
"field.name": "Name", "field.type": "Type", "field.url": "Catalog / API URL", "field.urlHint": "Optional for npm and GitHub", "source.remove": "Remove", "source.share": "Share", "source.shared": "Copied", "source.enabled": "Enabled", "source.disabled": "Disabled", "source.checking": "Checking", "source.online": "Status: online", "source.packages": "{count} packages", "source.repositories": "{count} repositories", "source.ok": "Healthy", "source.error": "Unavailable", "source.duplicate": "A source with this id already exists.", "source.required": "Name is required.", "source.urlRequired": "This source type requires a catalog URL.",
|
|
465
|
+
"field.name": "Name", "field.type": "Type", "field.url": "Catalog / API URL", "field.urlHint": "Optional for npm and GitHub", "source.remove": "Remove", "source.share": "Share", "source.shared": "Copied", "source.enabled": "Enabled", "source.disabled": "Disabled", "source.checking": "Checking", "source.online": "Status: online", "source.packages": "{count} packages", "source.repositories": "{count} repositories", "source.countingPackages": "Counting packages…", "source.countingRepositories": "Counting repositories…", "source.ok": "Healthy", "source.error": "Unavailable", "source.duplicate": "A source with this id already exists.", "source.required": "Name is required.", "source.urlRequired": "This source type requires a catalog URL.",
|
|
462
466
|
"activation.title": "Registry Aggregator ready", "activation.body": "npm and GitHub sources are connected by default. Open Registry Aggregator to search them together or add more sources.", "activation.open": "Open Registry Aggregator", "activation.later": "Later",
|
|
463
|
-
"browse.search": "Search plugins, packages, repositories…", "browse.refresh": "Refresh", "browse.pageSize": "Per page", "browse.page": "Page {page} of {pages}", "browse.prev": "Previous", "browse.next": "Next", "browse.freshness": "Freshness", "browse.freshness.any": "Any age", "browse.freshness.30": "≤ 30 days", "browse.freshness.90": "≤ 90 days", "browse.freshness.365": "≤ 1 year", "browse.category": "Category", "browse.category.any": "All categories", "browse.dshMetadata": "DSH metadata", "browse.dshMetadata.any": "Any DSH metadata", "browse.dshMetadata.declared": "DSH declared", "browse.dshMetadata.unknown": "DSH unknown", "install": "Install", "install.confirm": "Install?", "installing": "Installing…", "install.checking": "Checking package…", "install.starting": "Starting install…", "install.running": "Installing package…", "install.runningAttempt": "Installing {index}/{total}…", "install.applying": "Applying plugin…", "installed": "Installed", "install.failed": "Install failed", "install.retry": "Retry install", "install.notBundle": "Not a DSH bundle", "install.approvalRequired": "Build-script approval required in the native Plugin Manager", "install.cancel": "Cancel", "description.more": "…", "description.expand": "Expand description", "description.less": "Less", "browse.empty": "No plugins found", "browse.sort": "Sort", "browse.sort.relevance": "Relevance", "browse.sort.stars": "Stars", "browse.sort.downloads": "Downloads", "browse.sort.freshness": "Freshest release", "browse.sort.starsDownloads": "Stars + downloads", "browse.sort.downloadsFreshness": "Downloads + freshness", "browse.sort.name": "Name", "browse.sort.priority": "Sort priority {priority}", "browse.sort.off": "Not active", "browse.sort.asc": "Ascending", "browse.sort.desc": "Descending", "browse.stableOnly": "Stable only", "browse.emptyHint": "No plugins match the current search or filters.", "browse.noSources": "No enabled registry sources. Open Sources and enable or add one.", "browse.sourcesUnavailable": "All enabled registry sources are unavailable.", "browse.loading": "Loading catalogs…", "browse.updating": "Updating results…", "browse.error": "Catalog request failed", "browse.results": "{count} results", "compat.check": "Check DSH compatibility", "compat.none": "DSH not declared", "compat.loading": "DSH …", "compat.apiPeers": "DSH API peers", "released": "released {age}", "repoUpdated": "repo {age}", "deprecated": "deprecated", "copy": "Copy command", "copied": "Copied", "copy.failed": "Could not copy", "install.warning": "Review the package and source before running this third-party install command.", "readonly": "Settings are read-only.", "unavailable": "Plugin source settings are unavailable.",
|
|
467
|
+
"browse.search": "Search plugins, packages, repositories…", "browse.refresh": "Refresh", "browse.pageSize": "Per page", "browse.page": "Page {page} of {pages}", "browse.prev": "Previous", "browse.next": "Next", "browse.freshness": "Freshness", "browse.freshness.any": "Any age", "browse.freshness.30": "≤ 30 days", "browse.freshness.90": "≤ 90 days", "browse.freshness.365": "≤ 1 year", "browse.category": "Category", "browse.category.any": "All categories", "browse.dshMetadata": "DSH metadata", "browse.dshMetadata.any": "Any DSH metadata", "browse.dshMetadata.declared": "DSH declared", "browse.dshMetadata.unknown": "DSH unknown", "install": "Install", "install.confirm": "Install?", "installing": "Installing…", "install.checking": "Checking package…", "install.starting": "Starting install…", "install.running": "Installing package…", "install.runningAttempt": "Installing {index}/{total}…", "install.applying": "Applying plugin…", "install.cancelActive": "Cancel install", "install.cancelling": "Cancelling…", "install.cancelFailed": "Cancel failed", "installed": "Installed", "installed.version": "Installed {version}", "update.available": "Update available {installed} → {available}", "install.failed": "Install failed", "install.retry": "Retry install", "install.notBundle": "Not a DSH bundle", "install.approvalRequired": "Build-script approval required in the native Plugin Manager", "install.cancel": "Cancel", "description.more": "…", "description.expand": "Expand description", "description.less": "Less", "browse.empty": "No plugins found", "browse.sort": "Sort", "browse.sort.relevance": "Relevance", "browse.sort.stars": "Stars", "browse.sort.downloads": "Downloads", "browse.sort.freshness": "Freshest release", "browse.sort.starsDownloads": "Stars + downloads", "browse.sort.downloadsFreshness": "Downloads + freshness", "browse.sort.name": "Name", "browse.sort.priority": "Sort priority {priority}", "browse.sort.off": "Not active", "browse.sort.asc": "Ascending", "browse.sort.desc": "Descending", "browse.stableOnly": "Stable only", "browse.emptyHint": "No plugins match the current search or filters.", "browse.noSources": "No enabled registry sources. Open Sources and enable or add one.", "browse.sourcesUnavailable": "All enabled registry sources are unavailable.", "browse.loading": "Loading catalogs…", "browse.updating": "Updating results…", "browse.error": "Catalog request failed", "browse.results": "{count} results", "compat.check": "Check DSH compatibility", "compat.none": "DSH not declared", "compat.loading": "DSH …", "compat.apiPeers": "DSH API peers", "released": "released {age}", "repoUpdated": "repo {age}", "deprecated": "deprecated", "copy": "Copy command", "copied": "Copied", "copy.failed": "Could not copy", "install.warning": "Review the package and source before running this third-party install command.", "readonly": "Settings are read-only.", "unavailable": "Plugin source settings are unavailable.",
|
|
464
468
|
};
|
|
465
469
|
const zh = {
|
|
466
470
|
nav: "注册源聚合器", kicker: "联合插件注册表", title: "注册源聚合器", lead: "聚合已连接的插件注册表和目录,统一搜索,并通过原生 DSH Plugin Manager 安装。",
|
|
467
471
|
"tab.sources": "来源", "tab.browse": "浏览", "sources.title": "已连接来源", "sources.add": "添加来源", "sources.addTitle": "添加新来源", "sources.addLead": "连接软件包注册表以发现和安装插件。", "sources.manage": "管理已连接的注册表并发现可用插件。", "sources.empty": "尚未连接插件源", "sources.emptyHint": "添加目录、npm 注册表或 GitHub 来源以开始。",
|
|
468
|
-
"field.name": "名称", "field.type": "类型", "field.url": "目录 / API URL", "field.urlHint": "npm 和 GitHub 可选", "source.remove": "移除", "source.share": "分享", "source.shared": "已复制", "source.enabled": "已启用", "source.disabled": "已禁用", "source.checking": "检查中", "source.online": "状态:在线", "source.packages": "{count} 个软件包", "source.repositories": "{count} 个仓库", "source.ok": "正常", "source.error": "不可用", "source.duplicate": "具有此 ID 的来源已存在。", "source.required": "名称为必填项。", "source.urlRequired": "此来源类型需要目录 URL。",
|
|
472
|
+
"field.name": "名称", "field.type": "类型", "field.url": "目录 / API URL", "field.urlHint": "npm 和 GitHub 可选", "source.remove": "移除", "source.share": "分享", "source.shared": "已复制", "source.enabled": "已启用", "source.disabled": "已禁用", "source.checking": "检查中", "source.online": "状态:在线", "source.packages": "{count} 个软件包", "source.repositories": "{count} 个仓库", "source.countingPackages": "正在统计软件包…", "source.countingRepositories": "正在统计仓库…", "source.ok": "正常", "source.error": "不可用", "source.duplicate": "具有此 ID 的来源已存在。", "source.required": "名称为必填项。", "source.urlRequired": "此来源类型需要目录 URL。",
|
|
469
473
|
"activation.title": "注册源聚合器已就绪", "activation.body": "npm 和 GitHub 来源默认已连接。打开注册源聚合器即可统一搜索或添加更多来源。", "activation.open": "打开注册源聚合器", "activation.later": "稍后",
|
|
470
|
-
"browse.search": "搜索插件、包、仓库…", "browse.refresh": "刷新", "browse.pageSize": "每页", "browse.page": "第 {page} / {pages} 页", "browse.prev": "上一页", "browse.next": "下一页", "browse.freshness": "新鲜度", "browse.freshness.any": "不限时间", "browse.freshness.30": "≤ 30 天", "browse.freshness.90": "≤ 90 天", "browse.freshness.365": "≤ 1 年", "browse.category": "类别", "browse.category.any": "全部类别", "browse.dshMetadata": "DSH 元数据", "browse.dshMetadata.any": "任意 DSH 元数据", "browse.dshMetadata.declared": "已声明 DSH", "browse.dshMetadata.unknown": "DSH 未知", "install": "安装", "install.confirm": "确认安装?", "installing": "安装中…", "installed": "已安装", "install.failed": "安装失败", "install.retry": "重试安装", "install.cancel": "取消", "description.more": "…", "description.expand": "展开描述", "description.less": "收起", "browse.empty": "未找到插件", "browse.sort": "排序", "browse.sort.relevance": "相关性", "browse.sort.stars": "星标", "browse.sort.downloads": "下载量", "browse.sort.freshness": "最新发布", "browse.sort.starsDownloads": "星标 + 下载量", "browse.sort.downloadsFreshness": "下载量 + 新鲜度", "browse.sort.name": "名称", "browse.sort.priority": "排序优先级 {priority}", "browse.sort.off": "未启用", "browse.sort.asc": "升序", "browse.sort.desc": "降序", "browse.stableOnly": "仅稳定版", "browse.emptyHint": "当前搜索或筛选条件没有匹配的插件。", "browse.noSources": "没有启用的注册源。请打开“来源”并启用或添加一个来源。", "browse.sourcesUnavailable": "所有已启用的注册源当前都不可用。", "browse.loading": "正在加载目录…", "browse.updating": "正在更新结果…", "browse.error": "目录请求失败", "browse.results": "{count} 个结果", "compat.check": "检查 DSH 兼容性", "compat.none": "未声明 DSH", "compat.loading": "DSH …", "compat.apiPeers": "DSH API 依赖", "released": "发布 {age}", "repoUpdated": "仓库 {age}", "deprecated": "已弃用", "copy": "复制命令", "copied": "已复制", "copy.failed": "复制失败", "install.warning": "运行此第三方安装命令前,请检查包和来源。", "readonly": "设置为只读。", "unavailable": "插件源设置不可用。",
|
|
474
|
+
"browse.search": "搜索插件、包、仓库…", "browse.refresh": "刷新", "browse.pageSize": "每页", "browse.page": "第 {page} / {pages} 页", "browse.prev": "上一页", "browse.next": "下一页", "browse.freshness": "新鲜度", "browse.freshness.any": "不限时间", "browse.freshness.30": "≤ 30 天", "browse.freshness.90": "≤ 90 天", "browse.freshness.365": "≤ 1 年", "browse.category": "类别", "browse.category.any": "全部类别", "browse.dshMetadata": "DSH 元数据", "browse.dshMetadata.any": "任意 DSH 元数据", "browse.dshMetadata.declared": "已声明 DSH", "browse.dshMetadata.unknown": "DSH 未知", "install": "安装", "install.confirm": "确认安装?", "installing": "安装中…", "install.cancelActive": "取消安装", "install.cancelling": "正在取消…", "install.cancelFailed": "取消失败", "installed": "已安装", "installed.version": "已安装 {version}", "update.available": "可更新 {installed} → {available}", "install.failed": "安装失败", "install.retry": "重试安装", "install.cancel": "取消", "description.more": "…", "description.expand": "展开描述", "description.less": "收起", "browse.empty": "未找到插件", "browse.sort": "排序", "browse.sort.relevance": "相关性", "browse.sort.stars": "星标", "browse.sort.downloads": "下载量", "browse.sort.freshness": "最新发布", "browse.sort.starsDownloads": "星标 + 下载量", "browse.sort.downloadsFreshness": "下载量 + 新鲜度", "browse.sort.name": "名称", "browse.sort.priority": "排序优先级 {priority}", "browse.sort.off": "未启用", "browse.sort.asc": "升序", "browse.sort.desc": "降序", "browse.stableOnly": "仅稳定版", "browse.emptyHint": "当前搜索或筛选条件没有匹配的插件。", "browse.noSources": "没有启用的注册源。请打开“来源”并启用或添加一个来源。", "browse.sourcesUnavailable": "所有已启用的注册源当前都不可用。", "browse.loading": "正在加载目录…", "browse.updating": "正在更新结果…", "browse.error": "目录请求失败", "browse.results": "{count} 个结果", "compat.check": "检查 DSH 兼容性", "compat.none": "未声明 DSH", "compat.loading": "DSH …", "compat.apiPeers": "DSH API 依赖", "released": "发布 {age}", "repoUpdated": "仓库 {age}", "deprecated": "已弃用", "copy": "复制命令", "copied": "已复制", "copy.failed": "复制失败", "install.warning": "运行此第三方安装命令前,请检查包和来源。", "readonly": "设置为只读。", "unavailable": "插件源设置不可用。",
|
|
471
475
|
};
|
|
472
476
|
|
|
473
477
|
function slug(value) {
|
|
@@ -595,12 +599,15 @@ window.__ModuleLoader__.load({
|
|
|
595
599
|
return h("div", { className: "pm-source-logo pm-source-logo-generic", "aria-hidden": true, children: h(PackageIcon, {}) });
|
|
596
600
|
}
|
|
597
601
|
|
|
598
|
-
function SourceRow({ source, health, t, busy, readOnly, onToggle, onRemove }) {
|
|
602
|
+
function SourceRow({ source, health, sourceCount, t, busy, readOnly, onToggle, onRemove }) {
|
|
599
603
|
const [open, setOpen] = react.useState(true);
|
|
600
604
|
const [copied, setCopied] = react.useState(false);
|
|
601
605
|
const status = source.enabled === false ? "off" : health?.ok === true ? "ok" : health?.ok === false ? "bad" : "wait";
|
|
602
606
|
const endpoint = sourceEndpoint(source);
|
|
603
|
-
const count = Number.isFinite(health?.count) ? health.count :
|
|
607
|
+
const count = Number.isFinite(sourceCount?.count) ? sourceCount.count : Number.isFinite(health?.count) ? health.count : undefined;
|
|
608
|
+
const countLoading = status === "ok" && sourceCount?.loading === true && count === undefined;
|
|
609
|
+
const countLabel = count === undefined ? undefined : source.type === "github" ? t("source.repositories", { count }) : t("source.packages", { count });
|
|
610
|
+
const countingLabel = source.type === "github" ? t("source.countingRepositories") : t("source.countingPackages");
|
|
604
611
|
const statusText = status === "off" ? t("source.disabled") : status === "ok" ? t("source.online") : status === "bad" ? t("source.error") : t("source.checking");
|
|
605
612
|
const copyEndpoint = async () => {
|
|
606
613
|
try { await globalThis.navigator?.clipboard?.writeText?.(endpoint); setCopied(true); setTimeout(() => setCopied(false), 1400); } catch {}
|
|
@@ -613,8 +620,9 @@ window.__ModuleLoader__.load({
|
|
|
613
620
|
h("div", { className: "pm-source-line", children: h("span", { className: "pm-source-name", children: source.name }) }),
|
|
614
621
|
hs("div", { className: "pm-source-evidence", children: [
|
|
615
622
|
hs("span", { className: "pm-source-evidence-item", children: [h("span", { className: `pm-status pm-status-${status}`, "aria-hidden": true }), h("span", { children: statusText })] }),
|
|
616
|
-
status === "ok" ? h("span", { className: "pm-evidence-separator", "aria-hidden": true }) : null,
|
|
617
|
-
|
|
623
|
+
status === "ok" && (count !== undefined || countLoading) ? h("span", { className: "pm-evidence-separator", "aria-hidden": true }) : null,
|
|
624
|
+
countLoading ? hs("span", { className: "pm-source-evidence-item", title: countingLabel, role: "status", "aria-label": countingLabel, children: [h(StateDot, { state: "ongoing", size: 14 }), h("span", { className: "pm-sr-only", children: countingLabel })] }) : null,
|
|
625
|
+
status === "ok" && count !== undefined ? hs("span", { className: "pm-source-evidence-item", children: [h(PackageIcon, {}), h("span", { children: countLabel })] }) : null,
|
|
618
626
|
] }),
|
|
619
627
|
] }),
|
|
620
628
|
] }),
|
|
@@ -632,12 +640,12 @@ window.__ModuleLoader__.load({
|
|
|
632
640
|
h("input", { className: "pm-source-url", value: endpoint, readOnly: true, title: endpoint }),
|
|
633
641
|
h("button", { type: "button", className: "pm-copy-url", onClick: copyEndpoint, title: copied ? t("copied") : t("copy"), "aria-label": copied ? t("copied") : t("copy"), children: copied ? h(IconCopyOutlineRegular, { size: 16 }) : h(IconCopyOutlineRegular, { size: 16 }) }),
|
|
634
642
|
] }),
|
|
635
|
-
health?.error ? h("div", { className: "pm-source-error", title: health.error, children: health.error }) : null,
|
|
643
|
+
health?.error ? h("div", { className: "pm-source-error", title: health.error, children: health.error }) : sourceCount?.error ? h("div", { className: "pm-source-error", title: sourceCount.error, children: sourceCount.error }) : null,
|
|
636
644
|
] }) : null,
|
|
637
645
|
] });
|
|
638
646
|
}
|
|
639
647
|
|
|
640
|
-
function SourcesView({ t, sources, writable, saveSources, busy, healthState, refreshHealth }) {
|
|
648
|
+
function SourcesView({ t, sources, writable, saveSources, busy, healthState, countMap, countsLoading, refreshHealth }) {
|
|
641
649
|
const [mutationError, setMutationError] = react.useState("");
|
|
642
650
|
const mutate = async (next) => {
|
|
643
651
|
setMutationError("");
|
|
@@ -652,12 +660,12 @@ window.__ModuleLoader__.load({
|
|
|
652
660
|
hs("div", { className: "pm-section-head-main", children: [
|
|
653
661
|
h("div", { children: [h("div", { className: "pm-section-title", children: t("sources.title") }), h("div", { className: "pm-section-lead", children: t("sources.manage") })] }),
|
|
654
662
|
] }),
|
|
655
|
-
h("button", { type: "button", className: "pm-refresh-icon", onClick: refreshHealth, disabled: healthState.loading, title: t("browse.refresh"), "aria-label": t("browse.refresh"), children: h(RefreshIcon, {}) }),
|
|
663
|
+
h("button", { type: "button", className: "pm-refresh-icon", onClick: refreshHealth, disabled: healthState.loading, title: t("browse.refresh"), "aria-label": t("browse.refresh"), children: h(RefreshIcon, { className: healthState.loading ? "pm-spin" : undefined }) }),
|
|
656
664
|
] }),
|
|
657
665
|
!writable ? h("p", { className: "pm-notice pm-connected-notice", children: t("readonly") }) : null,
|
|
658
666
|
mutationError ? h("p", { className: "pm-notice pm-notice-error pm-connected-notice", role: "alert", children: mutationError }) : null,
|
|
659
667
|
healthState.error ? h("p", { className: "pm-notice pm-notice-error pm-connected-notice", role: "alert", children: healthState.error }) : null,
|
|
660
|
-
sources.length === 0 ? h(Empty, { title: t("sources.empty"), hint: t("sources.emptyHint") }) : h("div", { className: "pm-source-list", children: sources.map((source) => h(SourceRow, { source, health: healthMap[source.id], t, busy, readOnly: !writable, onToggle: (id) => { void mutate(sources.map((row) => row.id === id ? { ...row, enabled: row.enabled === false } : row)); }, onRemove: (id) => { void mutate(sources.filter((row) => row.id !== id)); } }, source.id)) }),
|
|
668
|
+
sources.length === 0 ? h(Empty, { title: t("sources.empty"), hint: t("sources.emptyHint") }) : h("div", { className: "pm-source-list", children: sources.map((source) => h(SourceRow, { source, health: healthMap[source.id], sourceCount: countMap[source.id], t, busy, readOnly: !writable, onToggle: (id) => { void mutate(sources.map((row) => row.id === id ? { ...row, enabled: row.enabled === false } : row)); }, onRemove: (id) => { void mutate(sources.filter((row) => row.id !== id)); } }, source.id)) }),
|
|
661
669
|
] }),
|
|
662
670
|
] });
|
|
663
671
|
}
|
|
@@ -671,6 +679,99 @@ window.__ModuleLoader__.load({
|
|
|
671
679
|
return new Intl.NumberFormat(undefined, { notation: "compact", maximumFractionDigits: 1 }).format(value);
|
|
672
680
|
}
|
|
673
681
|
|
|
682
|
+
function parseSemver(value) {
|
|
683
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/u.exec(String(value ?? "").trim());
|
|
684
|
+
if (!match) return undefined;
|
|
685
|
+
return {
|
|
686
|
+
core: [Number(match[1]), Number(match[2]), Number(match[3])],
|
|
687
|
+
prerelease: match[4] ? match[4].split(".") : [],
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function compareSemver(left, right) {
|
|
692
|
+
const a = parseSemver(left);
|
|
693
|
+
const b = parseSemver(right);
|
|
694
|
+
if (!a || !b) return 0;
|
|
695
|
+
for (let index = 0; index < 3; index++) {
|
|
696
|
+
if (a.core[index] !== b.core[index]) return a.core[index] > b.core[index] ? 1 : -1;
|
|
697
|
+
}
|
|
698
|
+
if (!a.prerelease.length && !b.prerelease.length) return 0;
|
|
699
|
+
if (!a.prerelease.length) return 1;
|
|
700
|
+
if (!b.prerelease.length) return -1;
|
|
701
|
+
const length = Math.max(a.prerelease.length, b.prerelease.length);
|
|
702
|
+
for (let index = 0; index < length; index++) {
|
|
703
|
+
const leftPart = a.prerelease[index];
|
|
704
|
+
const rightPart = b.prerelease[index];
|
|
705
|
+
if (leftPart === undefined) return -1;
|
|
706
|
+
if (rightPart === undefined) return 1;
|
|
707
|
+
if (leftPart === rightPart) continue;
|
|
708
|
+
const leftNumeric = /^\d+$/u.test(leftPart);
|
|
709
|
+
const rightNumeric = /^\d+$/u.test(rightPart);
|
|
710
|
+
if (leftNumeric && rightNumeric) return Number(leftPart) > Number(rightPart) ? 1 : -1;
|
|
711
|
+
if (leftNumeric !== rightNumeric) return leftNumeric ? -1 : 1;
|
|
712
|
+
return leftPart > rightPart ? 1 : -1;
|
|
713
|
+
}
|
|
714
|
+
return 0;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function localSearchVariants(value) {
|
|
718
|
+
const needle = String(value ?? "").trim().toLowerCase();
|
|
719
|
+
if (!needle) return [];
|
|
720
|
+
const variants = [needle];
|
|
721
|
+
const unscoped = needle.startsWith("@") && needle.includes("/") ? needle.slice(needle.indexOf("/") + 1) : needle;
|
|
722
|
+
if (unscoped !== needle) variants.push(unscoped);
|
|
723
|
+
let shortened = unscoped;
|
|
724
|
+
for (const prefix of ["dsh-client-ui-", "dsh-ui-", "dsh-plugin-"]) {
|
|
725
|
+
if (shortened.startsWith(prefix) && shortened.length > prefix.length) {
|
|
726
|
+
shortened = shortened.slice(prefix.length);
|
|
727
|
+
variants.push(shortened);
|
|
728
|
+
break;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
if (shortened.startsWith("dsh-") && shortened.length > 4) variants.push(shortened.slice(4));
|
|
732
|
+
const parts = shortened.split("-").filter(Boolean);
|
|
733
|
+
if (parts.length >= 2) variants.push(parts.slice(-2).join("-"));
|
|
734
|
+
return [...new Set(variants.filter((item) => item.length >= 3))].slice(0, 3);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function localRelevanceScore(plugin, query) {
|
|
738
|
+
const variants = localSearchVariants(query);
|
|
739
|
+
if (!variants.length) return 0;
|
|
740
|
+
const name = String(plugin.name ?? "").toLowerCase();
|
|
741
|
+
const packageName = String(plugin.identity?.package ?? "").toLowerCase();
|
|
742
|
+
const repository = String(plugin.identity?.repository ?? "").toLowerCase();
|
|
743
|
+
const description = String(plugin.description ?? "").toLowerCase();
|
|
744
|
+
const tags = (plugin.tags ?? []).map((tag) => String(tag).toLowerCase());
|
|
745
|
+
let best = 0;
|
|
746
|
+
for (let index = 0; index < variants.length; index += 1) {
|
|
747
|
+
const needle = variants[index];
|
|
748
|
+
const penalty = index * 25;
|
|
749
|
+
const score = Math.max(
|
|
750
|
+
packageName === needle ? 1000 : 0,
|
|
751
|
+
name === needle ? 950 : 0,
|
|
752
|
+
packageName.startsWith(needle) ? 850 : 0,
|
|
753
|
+
name.startsWith(needle) ? 800 : 0,
|
|
754
|
+
packageName.includes(needle) ? 720 : 0,
|
|
755
|
+
name.includes(needle) ? 680 : 0,
|
|
756
|
+
tags.includes(needle) ? 600 : 0,
|
|
757
|
+
tags.some((tag) => tag.includes(needle)) ? 500 : 0,
|
|
758
|
+
repository.includes(needle) ? 300 : 0,
|
|
759
|
+
description.includes(needle) ? 200 : 0,
|
|
760
|
+
) - penalty;
|
|
761
|
+
best = Math.max(best, score);
|
|
762
|
+
}
|
|
763
|
+
return Math.max(0, best);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function rankVisiblePlugins(plugins, query, sorts) {
|
|
767
|
+
if (!Array.isArray(plugins) || plugins.length < 2 || (Array.isArray(sorts) && sorts.length) || !String(query ?? "").trim()) return plugins;
|
|
768
|
+
return [...plugins].sort((left, right) => {
|
|
769
|
+
const relevance = localRelevanceScore(right, query) - localRelevanceScore(left, query);
|
|
770
|
+
if (relevance !== 0) return relevance;
|
|
771
|
+
return String(left.name ?? "").localeCompare(String(right.name ?? ""));
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
|
|
674
775
|
function repositoryEvidenceKey(value) {
|
|
675
776
|
if (typeof value !== "string" || !value.trim()) return "";
|
|
676
777
|
let raw = value.trim().replace(/^git\+/, "").replace(/^git@github\.com:/i, "https://github.com/");
|
|
@@ -815,7 +916,7 @@ window.__ModuleLoader__.load({
|
|
|
815
916
|
] });
|
|
816
917
|
}
|
|
817
918
|
|
|
818
|
-
function PluginCard({ plugin, t }) {
|
|
919
|
+
function PluginCard({ plugin, t, bundles }) {
|
|
819
920
|
const [copyState, setCopyState] = react.useState("");
|
|
820
921
|
const [installState, setInstallState] = react.useState("idle");
|
|
821
922
|
const [installError, setInstallError] = react.useState("");
|
|
@@ -823,6 +924,16 @@ window.__ModuleLoader__.load({
|
|
|
823
924
|
const [detailsState, setDetailsState] = react.useState({ status: "idle" });
|
|
824
925
|
const command = installCommand(plugin);
|
|
825
926
|
const spec = plugin.install?.spec || "";
|
|
927
|
+
const packageName = plugin.identity?.package;
|
|
928
|
+
const installedBundle = packageName ? (bundles ?? []).find((bundle) => bundle?.name === packageName) : undefined;
|
|
929
|
+
const installedVersion = typeof installedBundle?.version === "string" ? installedBundle.version : undefined;
|
|
930
|
+
const updateAvailable = Boolean(installedVersion && plugin.version && compareSemver(plugin.version, installedVersion) === 1);
|
|
931
|
+
const effectiveInstallState = installedBundle ? "installed" : installState;
|
|
932
|
+
const installedLabel = updateAvailable
|
|
933
|
+
? t("update.available", { installed: installedVersion, available: plugin.version })
|
|
934
|
+
: installedVersion ? t("installed.version", { version: installedVersion })
|
|
935
|
+
: installState === "installed" && plugin.version ? t("installed.version", { version: plugin.version })
|
|
936
|
+
: t("installed");
|
|
826
937
|
const declaredCompatibility = plugin.evidence?.dshCompatibility;
|
|
827
938
|
const loadDetails = async () => {
|
|
828
939
|
if (detailsState.status === "loading" || detailsState.status === "ready") return;
|
|
@@ -868,7 +979,7 @@ window.__ModuleLoader__.load({
|
|
|
868
979
|
};
|
|
869
980
|
|
|
870
981
|
const install = async () => {
|
|
871
|
-
if (!spec || !remote?.pluginManager || installState === "installing" || installState === "installed" || installState === "invalid") return;
|
|
982
|
+
if (!spec || !remote?.pluginManager || installedBundle || installState === "installing" || installState === "installed" || installState === "invalid") return;
|
|
872
983
|
setInstallState("installing");
|
|
873
984
|
setInstallError("");
|
|
874
985
|
setInstallProgress({ phase: "checking", activity: 0 });
|
|
@@ -917,7 +1028,13 @@ window.__ModuleLoader__.load({
|
|
|
917
1028
|
};
|
|
918
1029
|
const result = await remote.pluginManager.installBundle(spec, options);
|
|
919
1030
|
if (!result?.ok) throw new Error(result?.error?.message || t("install.failed"));
|
|
920
|
-
if (result.value?.application === "
|
|
1031
|
+
if (result.value?.application === "cancelled") {
|
|
1032
|
+
setInstallProgress({ phase: "idle", activity: 0 });
|
|
1033
|
+
setInstallState("idle");
|
|
1034
|
+
setInstallError("");
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
if (result.value?.application === "failed") {
|
|
921
1038
|
const pending = result.value?.pendingBuilds;
|
|
922
1039
|
if (Array.isArray(pending) && pending.length) throw new Error(`${t("install.approvalRequired")}: ${pending.join(", ")}`);
|
|
923
1040
|
throw new Error(result.value?.error?.diagnostic || result.value?.packageResult?.output || t("install.failed"));
|
|
@@ -934,8 +1051,25 @@ window.__ModuleLoader__.load({
|
|
|
934
1051
|
}
|
|
935
1052
|
};
|
|
936
1053
|
|
|
1054
|
+
const cancelInstall = async () => {
|
|
1055
|
+
const requestId = installProgress.requestId;
|
|
1056
|
+
if (!requestId || installState !== "installing" || installProgress.phase === "applying" || installProgress.phase === "cancelling"
|
|
1057
|
+
|| typeof remote?.pluginManager?.cancelInstall !== "function") return;
|
|
1058
|
+
setInstallError("");
|
|
1059
|
+
setInstallProgress((current) => ({ ...current, phase: "cancelling" }));
|
|
1060
|
+
try {
|
|
1061
|
+
const result = await remote.pluginManager.cancelInstall(requestId);
|
|
1062
|
+
if (!result?.ok) throw new Error(result?.error?.message || t("install.cancelFailed"));
|
|
1063
|
+
if (result.value?.status === "too-late") {
|
|
1064
|
+
setInstallProgress((current) => ({ ...current, phase: "applying" }));
|
|
1065
|
+
}
|
|
1066
|
+
} catch (error) {
|
|
1067
|
+
setInstallProgress((current) => ({ ...current, phase: "installing" }));
|
|
1068
|
+
setInstallError(`${t("install.cancelFailed")}: ${String(error?.message ?? error)}`);
|
|
1069
|
+
}
|
|
1070
|
+
};
|
|
1071
|
+
|
|
937
1072
|
const sources = plugin.sources ?? [];
|
|
938
|
-
const primarySource = sources[0];
|
|
939
1073
|
const requiresVerification = sources.some((source) => source.type === "npm" || source.type === "github");
|
|
940
1074
|
const installability = detailsState.data?.installability ?? plugin.evidence?.installability ?? (requiresVerification ? "unknown" : "bundle");
|
|
941
1075
|
const installAllowed = Boolean(spec) && (!requiresVerification || installability === "bundle");
|
|
@@ -947,15 +1081,20 @@ window.__ModuleLoader__.load({
|
|
|
947
1081
|
? installProgress.attempt
|
|
948
1082
|
? t("install.runningAttempt", { index: installProgress.attempt.index, total: installProgress.attempt.total })
|
|
949
1083
|
: t("install.running")
|
|
950
|
-
: installProgress.phase === "
|
|
951
|
-
: t("
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
1084
|
+
: installProgress.phase === "cancelling" ? t("install.cancelling")
|
|
1085
|
+
: installProgress.phase === "applying" ? t("install.applying")
|
|
1086
|
+
: t("installing");
|
|
1087
|
+
const canCancelInstall = effectiveInstallState === "installing"
|
|
1088
|
+
&& Boolean(installProgress.requestId)
|
|
1089
|
+
&& !["applying", "cancelling"].includes(installProgress.phase)
|
|
1090
|
+
&& typeof remote?.pluginManager?.cancelInstall === "function";
|
|
1091
|
+
const installTitle = effectiveInstallState === "installing" ? progressTitle
|
|
1092
|
+
: effectiveInstallState === "installed" ? installedLabel
|
|
1093
|
+
: effectiveInstallState === "invalid" ? t("install.notBundle")
|
|
1094
|
+
: effectiveInstallState === "failed" ? t("install.retry") : t("install");
|
|
1095
|
+
const installIcon = effectiveInstallState === "installing" ? h(StateDot, { state: "ongoing", size: 16 })
|
|
1096
|
+
: effectiveInstallState === "installed" ? h(IconCheckOutlineRegular, { size: 16 })
|
|
1097
|
+
: effectiveInstallState === "failed" || effectiveInstallState === "invalid" ? h(IconWarningOutlineRegular, { size: 16 })
|
|
959
1098
|
: h(IconDownloadOutlineRegular, { size: 16 });
|
|
960
1099
|
|
|
961
1100
|
return hs("article", { className: "pm-card", children: [
|
|
@@ -977,11 +1116,15 @@ window.__ModuleLoader__.load({
|
|
|
977
1116
|
plugin.evidence?.rating !== undefined ? hs("span", { className: "pm-signal", title: plugin.evidence.ratingCount !== undefined ? `${plugin.evidence.ratingCount} ratings` : "Source rating", children: [h("span", { className: "pm-star-icon", children: "★" }), h("span", { children: `${plugin.evidence.rating.toFixed(1)}${plugin.evidence.ratingCount !== undefined ? ` (${compactNumber(plugin.evidence.ratingCount)})` : ""}` })] }) : null,
|
|
978
1117
|
] }),
|
|
979
1118
|
] }),
|
|
980
|
-
installAllowed ?
|
|
1119
|
+
installAllowed ? hs("div", { className: "pm-card-install-actions", children: [
|
|
1120
|
+
h("button", { type: "button", className: "pm-card-install", "data-state": effectiveInstallState, disabled: effectiveInstallState === "installing" || effectiveInstallState === "installed" || effectiveInstallState === "invalid", onClick: install, title: installTitle, "aria-label": installTitle, children: installIcon }),
|
|
1121
|
+
canCancelInstall ? h("button", { type: "button", className: "pm-card-cancel", onClick: () => { void cancelInstall(); }, title: t("install.cancelActive"), "aria-label": t("install.cancelActive"), children: h(IconCloseOutlineRegular, { size: 14 }) }) : null,
|
|
1122
|
+
] }) : null,
|
|
981
1123
|
] }),
|
|
982
1124
|
plugin.description ? h(ClampedDescription, { text: plugin.description, t, onExpand: loadDetails }) : null,
|
|
983
1125
|
hs("div", { className: "pm-card-meta", children: [
|
|
984
|
-
|
|
1126
|
+
effectiveInstallState === "installed" ? h("span", { className: `pm-bundle-state ${updateAvailable ? "pm-bundle-state-unknown" : "pm-bundle-state-bundle"}`, title: installedLabel, children: installedLabel }) : null,
|
|
1127
|
+
...sources.map((source) => h("span", { className: "pm-badge", children: source.name }, source.id)),
|
|
985
1128
|
...(plugin.tags ?? []).map((tag) => h("span", { className: "pm-tag", children: tag }, tag)),
|
|
986
1129
|
requiresVerification ? h("span", { className: `pm-bundle-state pm-bundle-state-${installability}`, title: bundleLabel, children: bundleLabel }) : null,
|
|
987
1130
|
h("button", { type: "button", className: "pm-compat pm-compat-button", title: resolvedCompatibility ? `DSH ${resolvedCompatibility}` : peerTitle || t("compat.check"), onClick: () => { void loadDetails(); }, children: compatibilityLabel }),
|
|
@@ -994,6 +1137,7 @@ window.__ModuleLoader__.load({
|
|
|
994
1137
|
h("button", { type: "button", className: "pm-icon-btn", onClick: copy, title: copyState === "ok" ? t("copied") : t("copy"), "aria-label": copyState === "ok" ? t("copied") : t("copy"), children: h(IconCopyOutlineRegular, { size: 16 }) }),
|
|
995
1138
|
] }) : null,
|
|
996
1139
|
installState === "failed" ? h("div", { className: "pm-install-status", title: installError, children: `${t("install.failed")}: ${installError}` }) : null,
|
|
1140
|
+
installState === "installing" && installError ? h("div", { className: "pm-install-status", title: installError, children: installError }) : null,
|
|
997
1141
|
installState === "invalid" ? h("div", { className: "pm-install-status", title: installError, children: `${t("install.notBundle")}: ${installError}` }) : null,
|
|
998
1142
|
h("span", { role: "status", "aria-live": "polite", className: "pm-sr-only", children: copyState === "ok" ? t("copied") : copyState === "error" ? t("copy.failed") : installTitle }),
|
|
999
1143
|
] });
|
|
@@ -1031,17 +1175,27 @@ window.__ModuleLoader__.load({
|
|
|
1031
1175
|
});
|
|
1032
1176
|
}
|
|
1033
1177
|
|
|
1034
|
-
function BrowseView({ t, state, query, setQuery, refresh, sorts, setSorts, stableOnly, setStableOnly, freshnessDays, setFreshnessDays, tag, setTag, dshMetadata, setDshMetadata, page, setPage, pageSize, setPageSize }) {
|
|
1178
|
+
function BrowseView({ t, state, query, setQuery, refresh, sorts, setSorts, stableOnly, setStableOnly, freshnessDays, setFreshnessDays, tag, setTag, dshMetadata, setDshMetadata, page, setPage, pageSize, setPageSize, bundles }) {
|
|
1035
1179
|
const plugins = state.data?.plugins ?? [];
|
|
1036
1180
|
const total = state.data?.total ?? 0;
|
|
1037
1181
|
const pageCount = state.data?.pageCount ?? 1;
|
|
1038
1182
|
const currentPage = state.data?.page ?? page;
|
|
1183
|
+
const sourceRows = Array.isArray(state.data?.sources) ? state.data.sources : [];
|
|
1184
|
+
const githubSourceRow = sourceRows.find((row) => (row?.type ?? row?.source?.type) === "github");
|
|
1185
|
+
const githubSourceValue = githubSourceRow?.source ?? githubSourceRow;
|
|
1186
|
+
const githubSource = githubSourceValue ? {
|
|
1187
|
+
id: githubSourceValue.id,
|
|
1188
|
+
name: githubSourceValue.name ?? "GitHub",
|
|
1189
|
+
type: "github",
|
|
1190
|
+
...(githubSourceValue.url ? { url: githubSourceValue.url } : {}),
|
|
1191
|
+
} : undefined;
|
|
1039
1192
|
const [starEvidence, setStarEvidence] = react.useState({});
|
|
1040
|
-
const
|
|
1193
|
+
const visiblePlugins = rankVisiblePlugins(plugins, query, sorts);
|
|
1194
|
+
const starSignature = plugins.map((plugin) => `${repositoryEvidenceKey(plugin.identity?.repository)}:${plugin.evidence?.stars ?? ""}:${(plugin.sources ?? []).some((source) => source.type === "github") ? "github" : ""}`).join("|");
|
|
1041
1195
|
react.useEffect(() => {
|
|
1042
1196
|
if (state.loading || !plugins.length || !connection) return undefined;
|
|
1043
1197
|
const repositories = [...new Set(plugins
|
|
1044
|
-
.filter((plugin) => plugin.evidence?.stars === undefined)
|
|
1198
|
+
.filter((plugin) => plugin.evidence?.stars === undefined || !(plugin.sources ?? []).some((source) => source.type === "github"))
|
|
1045
1199
|
.map((plugin) => plugin.identity?.repository)
|
|
1046
1200
|
.filter((repository) => repositoryEvidenceKey(repository).startsWith("https://github.com/"))
|
|
1047
1201
|
)].slice(0, 20);
|
|
@@ -1053,8 +1207,12 @@ window.__ModuleLoader__.load({
|
|
|
1053
1207
|
const next = { ...current };
|
|
1054
1208
|
for (const row of rows) {
|
|
1055
1209
|
const key = repositoryEvidenceKey(row?.repository);
|
|
1056
|
-
if (!key
|
|
1057
|
-
next[key] = {
|
|
1210
|
+
if (!key) continue;
|
|
1211
|
+
next[key] = {
|
|
1212
|
+
...(typeof row?.stars === "number" ? { stars: row.stars } : {}),
|
|
1213
|
+
...(row?.repositoryUpdatedAt ? { repositoryUpdatedAt: row.repositoryUpdatedAt } : {}),
|
|
1214
|
+
discoveryEligible: row?.discoveryEligible === true,
|
|
1215
|
+
};
|
|
1058
1216
|
}
|
|
1059
1217
|
return next;
|
|
1060
1218
|
});
|
|
@@ -1062,11 +1220,17 @@ window.__ModuleLoader__.load({
|
|
|
1062
1220
|
return () => controller.abort();
|
|
1063
1221
|
}, [starSignature]);
|
|
1064
1222
|
const pluginWithAsyncStars = (plugin) => {
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1223
|
+
const asyncEvidence = starEvidence[repositoryEvidenceKey(plugin.identity?.repository)];
|
|
1224
|
+
if (!asyncEvidence) return plugin;
|
|
1225
|
+
const evidence = {
|
|
1226
|
+
...(plugin.evidence ?? {}),
|
|
1227
|
+
...(typeof asyncEvidence.stars === "number" ? { stars: asyncEvidence.stars } : {}),
|
|
1228
|
+
...(asyncEvidence.repositoryUpdatedAt ? { repositoryUpdatedAt: asyncEvidence.repositoryUpdatedAt } : {}),
|
|
1229
|
+
};
|
|
1230
|
+
const sources = [...(plugin.sources ?? [])];
|
|
1231
|
+
if (asyncEvidence.discoveryEligible && githubSource && !sources.some((source) => source.type === "github")) sources.push(githubSource);
|
|
1232
|
+
return { ...plugin, evidence, sources };
|
|
1068
1233
|
};
|
|
1069
|
-
const sourceRows = Array.isArray(state.data?.sources) ? state.data.sources : [];
|
|
1070
1234
|
const failedSources = sourceRows.filter((row) => row?.health?.ok === false);
|
|
1071
1235
|
const hasEnabledSources = sourceRows.length > 0;
|
|
1072
1236
|
const allSourcesFailed = hasEnabledSources && failedSources.length === sourceRows.length;
|
|
@@ -1137,7 +1301,7 @@ window.__ModuleLoader__.load({
|
|
|
1137
1301
|
children: `${row.name ?? row.id ?? row.source?.name ?? row.source?.id ?? "source"}: ${row.health?.error ?? t("source.error")}`,
|
|
1138
1302
|
}, row.source?.id ?? row.source?.name)) : null,
|
|
1139
1303
|
!state.loading && !state.error && plugins.length === 0 ? h(Empty, { title: t("browse.empty"), hint: emptyHint }) : null,
|
|
1140
|
-
plugins.length ? h("div", { className: "pm-results", "data-updating": state.loading, children:
|
|
1304
|
+
plugins.length ? h("div", { className: "pm-results", "data-updating": state.loading, children: visiblePlugins.map((plugin) => h(PluginCard, { plugin: pluginWithAsyncStars(plugin), t, bundles }, plugin.identity?.package || plugin.identity?.repository || plugin.identity?.fallback)) }) : null,
|
|
1141
1305
|
plugins.length ? h("div", { className: "pm-browse-bottomline", children: bottomPager }) : null,
|
|
1142
1306
|
] }) : null,
|
|
1143
1307
|
] });
|
|
@@ -1150,7 +1314,10 @@ window.__ModuleLoader__.load({
|
|
|
1150
1314
|
const [query, setQuery] = react.useState("");
|
|
1151
1315
|
const [busy, setBusy] = react.useState(false);
|
|
1152
1316
|
const [healthState, setHealthState] = react.useState({ loading: false });
|
|
1317
|
+
const [countMap, setCountMap] = react.useState({});
|
|
1318
|
+
const [countsLoading, setCountsLoading] = react.useState(false);
|
|
1153
1319
|
const [browseState, setBrowseState] = react.useState({ loading: false });
|
|
1320
|
+
const [bundleState, setBundleState] = react.useState({ loading: false, data: [] });
|
|
1154
1321
|
const [browseRevision, setBrowseRevision] = react.useState(0);
|
|
1155
1322
|
const [sorts, setSorts] = react.useState([]);
|
|
1156
1323
|
const [stableOnly, setStableOnly] = react.useState(false);
|
|
@@ -1194,6 +1361,56 @@ window.__ModuleLoader__.load({
|
|
|
1194
1361
|
return () => controller.abort();
|
|
1195
1362
|
}, [tab, signature, healthRevision, connectionRevision]);
|
|
1196
1363
|
|
|
1364
|
+
react.useEffect(() => {
|
|
1365
|
+
if (tab !== "browse" || typeof remote?.pluginManager?.listBundles !== "function") return undefined;
|
|
1366
|
+
let disposed = false;
|
|
1367
|
+
setBundleState((current) => ({ ...current, loading: true, error: undefined }));
|
|
1368
|
+
Promise.resolve(remote.pluginManager.listBundles()).then(
|
|
1369
|
+
(result) => {
|
|
1370
|
+
if (disposed) return;
|
|
1371
|
+
if (!result?.ok) {
|
|
1372
|
+
setBundleState({ loading: false, data: [], error: String(result?.error?.message ?? t("browse.error")) });
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
setBundleState({ loading: false, data: Array.isArray(result.value) ? result.value : [] });
|
|
1376
|
+
},
|
|
1377
|
+
(error) => { if (!disposed) setBundleState({ loading: false, data: [], error: String(error?.message ?? error) }); },
|
|
1378
|
+
);
|
|
1379
|
+
return () => { disposed = true; };
|
|
1380
|
+
}, [tab, connectionRevision]);
|
|
1381
|
+
|
|
1382
|
+
react.useEffect(() => {
|
|
1383
|
+
if (tab !== "sources" || !connection || healthState.loading || !healthState.data) return undefined;
|
|
1384
|
+
const healthRows = healthState.data.sources ?? [];
|
|
1385
|
+
const healthyIds = new Set(healthRows.filter((row) => row?.health?.ok === true).map((row) => row.source?.id).filter(Boolean));
|
|
1386
|
+
const targets = sources.filter((source) => source.enabled !== false && healthyIds.has(source.id));
|
|
1387
|
+
if (!targets.length) { setCountsLoading(false); return undefined; }
|
|
1388
|
+
const controller = new AbortController();
|
|
1389
|
+
let active = true;
|
|
1390
|
+
setCountsLoading(true);
|
|
1391
|
+
setCountMap((current) => {
|
|
1392
|
+
const next = { ...current };
|
|
1393
|
+
for (const source of targets) next[source.id] = { ...(next[source.id] ?? {}), loading: true, error: undefined };
|
|
1394
|
+
return next;
|
|
1395
|
+
});
|
|
1396
|
+
let pending = targets.length;
|
|
1397
|
+
for (const source of targets) {
|
|
1398
|
+
rpc("counts", { sourceId: source.id }, controller.signal).then((data) => {
|
|
1399
|
+
if (!active) return;
|
|
1400
|
+
const row = data?.sources?.[0];
|
|
1401
|
+
setCountMap((current) => ({ ...current, [source.id]: row ? { ...row, loading: false } : { source, loading: false, error: "source count unavailable" } }));
|
|
1402
|
+
}, (error) => {
|
|
1403
|
+
if (!active || error?.name === "AbortError") return;
|
|
1404
|
+
setCountMap((current) => ({ ...current, [source.id]: { source, loading: false, error: String(error?.message ?? error) } }));
|
|
1405
|
+
}).finally(() => {
|
|
1406
|
+
if (!active) return;
|
|
1407
|
+
pending -= 1;
|
|
1408
|
+
if (pending === 0) setCountsLoading(false);
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
1411
|
+
return () => { active = false; controller.abort(); setCountsLoading(false); };
|
|
1412
|
+
}, [tab, signature, healthRevision, connectionRevision, healthState.loading, healthState.data]);
|
|
1413
|
+
|
|
1197
1414
|
react.useEffect(() => {
|
|
1198
1415
|
if (tab !== "browse" || !connection) return undefined;
|
|
1199
1416
|
const controller = new AbortController();
|
|
@@ -1213,7 +1430,7 @@ window.__ModuleLoader__.load({
|
|
|
1213
1430
|
return h("div", { className: "pm-root", children: hs("section", { className: "pm-shell", "aria-labelledby": "pm-title", children: [
|
|
1214
1431
|
h("p", { className: "pm-kicker", children: t("kicker") }), h("h2", { id: "pm-title", className: "pm-title", children: t("title") }), h("p", { className: "pm-lead", children: t("lead") }),
|
|
1215
1432
|
hs("div", { className: "pm-tabs", role: "tablist", "aria-label": t("title"), children: [h("button", { id: "pm-tab-sources", type: "button", className: "pm-tab", role: "tab", tabIndex: tab === "sources" ? 0 : -1, "aria-controls": "pm-panel-sources", "aria-selected": tab === "sources", "data-active": tab === "sources", onKeyDown: tabKey, onClick: () => selectTab("sources"), children: hs("span", { className: "pm-tab-content", children: [h(DatabaseIcon, {}), h("span", { children: t("tab.sources") })] }) }), h("button", { id: "pm-tab-browse", type: "button", className: "pm-tab", role: "tab", tabIndex: tab === "browse" ? 0 : -1, "aria-controls": "pm-panel-browse", "aria-selected": tab === "browse", "data-active": tab === "browse", onKeyDown: tabKey, onClick: () => selectTab("browse"), children: hs("span", { className: "pm-tab-content", children: [h(GridIcon, {}), h("span", { children: t("tab.browse") })] }) })] }),
|
|
1216
|
-
h("div", { id: `pm-panel-${tab}`, role: "tabpanel", "aria-labelledby": `pm-tab-${tab}`, children: tab === "sources" ? h(SourcesView, { t, sources, writable, saveSources, busy, healthState, refreshHealth: () => setHealthRevision((value) => value + 1) }) : h(BrowseView, { t, state: browseState, query, setQuery, refresh: () => setBrowseRevision((value) => value + 1), sorts, setSorts, stableOnly, setStableOnly, freshnessDays, setFreshnessDays, tag, setTag, dshMetadata, setDshMetadata, page, setPage, pageSize, setPageSize }) }),
|
|
1433
|
+
h("div", { id: `pm-panel-${tab}`, role: "tabpanel", "aria-labelledby": `pm-tab-${tab}`, children: tab === "sources" ? h(SourcesView, { t, sources, writable, saveSources, busy, healthState, countMap, countsLoading, refreshHealth: () => setHealthRevision((value) => value + 1) }) : h(BrowseView, { t, state: browseState, query, setQuery, refresh: () => setBrowseRevision((value) => value + 1), sorts, setSorts, stableOnly, setStableOnly, freshnessDays, setFreshnessDays, tag, setTag, dshMetadata, setDshMetadata, page, setPage, pageSize, setPageSize, bundles: bundleState.data ?? [] }) }),
|
|
1217
1434
|
] }) });
|
|
1218
1435
|
}
|
|
1219
1436
|
|
package/lib/core.js
CHANGED
|
@@ -7,6 +7,10 @@ const DEFAULT_TIMEOUT_MS = 10_000;
|
|
|
7
7
|
const DEFAULT_MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
|
|
8
8
|
const DEFAULT_MAX_PLUGINS = 2_000;
|
|
9
9
|
const DEFAULT_MAX_REDIRECTS = 3;
|
|
10
|
+
const GITHUB_MAX_ATTEMPTS = 3;
|
|
11
|
+
const GITHUB_RETRY_BASE_DELAY_MS = 250;
|
|
12
|
+
const GITHUB_RETRY_TIMEOUT_MS = 5_000;
|
|
13
|
+
const RETRYABLE_HTTP_STATUSES = new Set([408, 429, 500, 502, 503, 504]);
|
|
10
14
|
const EVIDENCE_CACHE_TTL_MS = 10 * 60 * 1000;
|
|
11
15
|
const METADATA_CACHE_TTL_MS = 30 * 60 * 1000;
|
|
12
16
|
const npmDownloadCache = new Map();
|
|
@@ -62,6 +66,23 @@ export const GITHUB_DISCOVERY_QUERIES = Object.freeze([
|
|
|
62
66
|
"topic:deepseek-harness topic:dsh-plugins",
|
|
63
67
|
]);
|
|
64
68
|
|
|
69
|
+
function githubRepositoryMatchesDiscovery(repository) {
|
|
70
|
+
const topics = new Set((Array.isArray(repository?.topics) ? repository.topics : [])
|
|
71
|
+
.map((topic) => String(topic).trim().toLowerCase()).filter(Boolean));
|
|
72
|
+
return topics.has("deepseek-harness-plugin")
|
|
73
|
+
|| (topics.has("deepseek-harness") && (topics.has("dsh-plugin") || topics.has("dsh-plugins")));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function githubHealthUrl(source) {
|
|
77
|
+
const url = new URL(source?.url ?? "https://api.github.com/");
|
|
78
|
+
url.search = "";
|
|
79
|
+
url.hash = "";
|
|
80
|
+
let path = url.pathname.replace(/\/+$/u, "");
|
|
81
|
+
path = path.replace(/\/search\/repositories$/u, "");
|
|
82
|
+
url.pathname = `${path || ""}/rate_limit`;
|
|
83
|
+
return url;
|
|
84
|
+
}
|
|
85
|
+
|
|
65
86
|
function text(value) {
|
|
66
87
|
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
67
88
|
}
|
|
@@ -474,11 +495,52 @@ async function readResponseBytes(response, maxBytes) {
|
|
|
474
495
|
return joined;
|
|
475
496
|
}
|
|
476
497
|
|
|
477
|
-
|
|
498
|
+
function retryableSourceError(message, details = {}) {
|
|
499
|
+
const error = new Error(message);
|
|
500
|
+
error.retryable = true;
|
|
501
|
+
if (details.status !== undefined) error.status = details.status;
|
|
502
|
+
if (details.retryAfterMs !== undefined) error.retryAfterMs = details.retryAfterMs;
|
|
503
|
+
return error;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function githubRateLimitRetryMs(response) {
|
|
507
|
+
if (response.status !== 403) return undefined;
|
|
508
|
+
const retryAfterHeader = response.headers.get("retry-after");
|
|
509
|
+
if (retryAfterHeader !== null) {
|
|
510
|
+
const retryAfter = Number(retryAfterHeader);
|
|
511
|
+
if (Number.isFinite(retryAfter) && retryAfter >= 0) return Math.min(retryAfter * 1000, GITHUB_RETRY_TIMEOUT_MS);
|
|
512
|
+
}
|
|
513
|
+
if (response.headers.get("x-ratelimit-remaining") !== "0") return undefined;
|
|
514
|
+
const resetHeader = response.headers.get("x-ratelimit-reset");
|
|
515
|
+
if (resetHeader === null) return undefined;
|
|
516
|
+
const resetSeconds = Number(resetHeader);
|
|
517
|
+
if (!Number.isFinite(resetSeconds)) return undefined;
|
|
518
|
+
const delayMs = Math.max(0, resetSeconds * 1000 - Date.now());
|
|
519
|
+
return delayMs <= GITHUB_RETRY_TIMEOUT_MS ? delayMs : undefined;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function waitForRetry(delayMs, signal) {
|
|
523
|
+
if (delayMs <= 0) return Promise.resolve();
|
|
524
|
+
if (signal?.aborted) return Promise.reject(new Error("source request aborted"));
|
|
525
|
+
return new Promise((resolve, reject) => {
|
|
526
|
+
const timer = setTimeout(done, delayMs);
|
|
527
|
+
function done() {
|
|
528
|
+
signal?.removeEventListener("abort", aborted);
|
|
529
|
+
resolve();
|
|
530
|
+
}
|
|
531
|
+
function aborted() {
|
|
532
|
+
clearTimeout(timer);
|
|
533
|
+
signal?.removeEventListener("abort", aborted);
|
|
534
|
+
reject(new Error("source request aborted"));
|
|
535
|
+
}
|
|
536
|
+
signal?.addEventListener("abort", aborted, { once: true });
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
async function fetchJsonAttempt(urlInput, source, options, timeoutMs) {
|
|
478
541
|
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
479
542
|
if (typeof fetchImpl !== "function") throw new Error("Host fetch is unavailable");
|
|
480
543
|
const resolveHost = options.resolveHost ?? defaultResolveHost;
|
|
481
|
-
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
482
544
|
const maxBytes = options.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES;
|
|
483
545
|
const maxRedirects = options.maxRedirects ?? DEFAULT_MAX_REDIRECTS;
|
|
484
546
|
const authToken = text(options.authToken);
|
|
@@ -499,9 +561,9 @@ export async function fetchJson(urlInput, source, options = {}) {
|
|
|
499
561
|
try {
|
|
500
562
|
response = await fetchImpl(url, { method: "GET", headers, redirect: "manual", signal: controller.signal, dispatcher });
|
|
501
563
|
} catch (error) {
|
|
502
|
-
if (timedOut) throw
|
|
564
|
+
if (timedOut) throw retryableSourceError("source request timed out");
|
|
503
565
|
if (options.signal?.aborted) throw new Error("source request aborted");
|
|
504
|
-
throw
|
|
566
|
+
throw retryableSourceError(`source request failed: ${String(error?.message ?? error)}`);
|
|
505
567
|
}
|
|
506
568
|
if ([301, 302, 303, 307, 308].includes(response.status)) {
|
|
507
569
|
const location = response.headers.get("location");
|
|
@@ -514,14 +576,18 @@ export async function fetchJson(urlInput, source, options = {}) {
|
|
|
514
576
|
continue;
|
|
515
577
|
}
|
|
516
578
|
if (!response.ok) {
|
|
579
|
+
const rateLimitRetryMs = source?.type === "github" ? githubRateLimitRetryMs(response) : undefined;
|
|
517
580
|
await response.body?.cancel?.();
|
|
518
|
-
|
|
581
|
+
const message = `source returned HTTP ${response.status}`;
|
|
582
|
+
if (RETRYABLE_HTTP_STATUSES.has(response.status)) throw retryableSourceError(message, { status: response.status });
|
|
583
|
+
if (rateLimitRetryMs !== undefined) throw retryableSourceError(message, { status: response.status, retryAfterMs: rateLimitRetryMs });
|
|
584
|
+
throw new Error(message);
|
|
519
585
|
}
|
|
520
586
|
let bytes;
|
|
521
587
|
try {
|
|
522
588
|
bytes = await readResponseBytes(response, maxBytes);
|
|
523
589
|
} catch (error) {
|
|
524
|
-
if (timedOut) throw
|
|
590
|
+
if (timedOut) throw retryableSourceError("source request timed out");
|
|
525
591
|
if (options.signal?.aborted) throw new Error("source request aborted");
|
|
526
592
|
throw error;
|
|
527
593
|
}
|
|
@@ -536,7 +602,7 @@ export async function fetchJson(urlInput, source, options = {}) {
|
|
|
536
602
|
}
|
|
537
603
|
throw new Error("source redirected too many times");
|
|
538
604
|
} catch (error) {
|
|
539
|
-
if (timedOut) throw
|
|
605
|
+
if (timedOut) throw retryableSourceError("source request timed out");
|
|
540
606
|
if (options.signal?.aborted) throw new Error("source request aborted");
|
|
541
607
|
throw error;
|
|
542
608
|
} finally {
|
|
@@ -545,6 +611,29 @@ export async function fetchJson(urlInput, source, options = {}) {
|
|
|
545
611
|
}
|
|
546
612
|
}
|
|
547
613
|
|
|
614
|
+
export async function fetchJson(urlInput, source, options = {}) {
|
|
615
|
+
const configuredAttempts = Number.isInteger(options.githubRetryAttempts) ? options.githubRetryAttempts : GITHUB_MAX_ATTEMPTS;
|
|
616
|
+
const maxAttempts = source?.type === "github" ? Math.min(Math.max(1, configuredAttempts), GITHUB_MAX_ATTEMPTS) : 1;
|
|
617
|
+
const firstTimeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
618
|
+
const retryTimeoutMs = Math.min(firstTimeoutMs, options.githubRetryTimeoutMs ?? GITHUB_RETRY_TIMEOUT_MS);
|
|
619
|
+
const baseDelayMs = Number.isFinite(options.githubRetryDelayMs) ? Math.max(0, options.githubRetryDelayMs) : GITHUB_RETRY_BASE_DELAY_MS;
|
|
620
|
+
let lastError;
|
|
621
|
+
|
|
622
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
623
|
+
if (options.signal?.aborted) throw new Error("source request aborted");
|
|
624
|
+
try {
|
|
625
|
+
const timeoutMs = attempt === 0 ? firstTimeoutMs : retryTimeoutMs;
|
|
626
|
+
return await fetchJsonAttempt(urlInput, source, options, timeoutMs);
|
|
627
|
+
} catch (error) {
|
|
628
|
+
lastError = error;
|
|
629
|
+
if (options.signal?.aborted || error?.retryable !== true || attempt + 1 >= maxAttempts) throw error;
|
|
630
|
+
const retryDelayMs = Math.max(baseDelayMs * (2 ** attempt), Number(error?.retryAfterMs) || 0);
|
|
631
|
+
await waitForRetry(Math.min(retryDelayMs, GITHUB_RETRY_TIMEOUT_MS), options.signal);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
throw lastError;
|
|
635
|
+
}
|
|
636
|
+
|
|
548
637
|
function catalogRows(manifest) {
|
|
549
638
|
if (Array.isArray(manifest)) return manifest;
|
|
550
639
|
if (manifest && typeof manifest === "object") {
|
|
@@ -742,11 +831,22 @@ export function createAdapter(sourceInput, options = {}) {
|
|
|
742
831
|
async health() {
|
|
743
832
|
const started = Date.now();
|
|
744
833
|
try {
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
834
|
+
if (source.type === "npm") {
|
|
835
|
+
const base = source.url && /\/-\/v1\/search\/?$/u.test(new URL(source.url).pathname)
|
|
836
|
+
? source.url
|
|
837
|
+
: new URL("-/v1/search", npmRegistryBase(source.url)).toString();
|
|
838
|
+
const url = new URL(base);
|
|
839
|
+
url.searchParams.set("text", `keywords:${NPM_DISCOVERY_KEYWORDS[0]}`);
|
|
840
|
+
url.searchParams.set("size", "1");
|
|
841
|
+
npmRows(await request(url), { requireDiscoveryKeyword: true });
|
|
842
|
+
return { ok: true, latencyMs: Date.now() - started };
|
|
843
|
+
}
|
|
844
|
+
if (source.type === "github") {
|
|
845
|
+
const value = await request(githubHealthUrl(source));
|
|
846
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("GitHub health response is malformed");
|
|
847
|
+
return { ok: true, latencyMs: Date.now() - started };
|
|
749
848
|
}
|
|
849
|
+
const plugins = await list();
|
|
750
850
|
return { ok: true, latencyMs: Date.now() - started, count: plugins.length };
|
|
751
851
|
} catch (error) {
|
|
752
852
|
return { ok: false, latencyMs: Date.now() - started, error: String(error?.message ?? error) };
|
|
@@ -789,6 +889,24 @@ export async function healthSources(sourceInputs, options = {}) {
|
|
|
789
889
|
}));
|
|
790
890
|
}
|
|
791
891
|
|
|
892
|
+
export async function countSources(sourceInputs, options = {}) {
|
|
893
|
+
const sources = validatedSources(sourceInputs, options);
|
|
894
|
+
return mapConcurrent(sources, options.concurrency ?? 4, async (source) => {
|
|
895
|
+
const attribution = sourceAttribution(source);
|
|
896
|
+
if (!source.enabled) return { source: attribution, disabled: true };
|
|
897
|
+
try {
|
|
898
|
+
let plugins = await createAdapter(source, options).list();
|
|
899
|
+
if ((source.type === "npm" || source.type === "github") && shouldVerifyRegistryBundles(options)) {
|
|
900
|
+
plugins = await enrichInstallability(plugins, options);
|
|
901
|
+
plugins = plugins.filter((plugin) => plugin.evidence?.installability === "bundle");
|
|
902
|
+
}
|
|
903
|
+
return { source: attribution, count: plugins.length };
|
|
904
|
+
} catch (error) {
|
|
905
|
+
return { source: attribution, error: String(error?.message ?? error) };
|
|
906
|
+
}
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
|
|
792
910
|
async function enrichNpmDownloads(plugins, options = {}) {
|
|
793
911
|
const packages = [...new Set(plugins.map((plugin) => plugin.identity?.package).filter(Boolean))].slice(0, options.maxEvidencePackages ?? 100);
|
|
794
912
|
if (!packages.length || options.enrichDownloads === false) return plugins;
|
|
@@ -911,6 +1029,7 @@ export async function githubRepositoryStats(repositories, options = {}) {
|
|
|
911
1029
|
const stats = {
|
|
912
1030
|
...(finiteNumber(value?.stargazers_count) !== undefined ? { stars: finiteNumber(value.stargazers_count) } : {}),
|
|
913
1031
|
...(isoDate(value?.pushed_at ?? value?.updated_at) ? { repositoryUpdatedAt: isoDate(value.pushed_at ?? value.updated_at) } : {}),
|
|
1032
|
+
discoveryEligible: githubRepositoryMatchesDiscovery(value),
|
|
914
1033
|
};
|
|
915
1034
|
cacheValue(githubStatsCache, key, stats);
|
|
916
1035
|
return { repository, ...stats };
|
package/lib/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import z from "@deepseek-ai/schemastery";
|
|
2
|
-
import { browseSources, FILTER_TAGS, githubPluginDetails, githubRepositoryStats, healthSources, npmPluginDetails, SOURCE_TYPES } from "./core.js";
|
|
2
|
+
import { browseSources, countSources, FILTER_TAGS, githubPluginDetails, githubRepositoryStats, healthSources, npmPluginDetails, SOURCE_TYPES } from "./core.js";
|
|
3
3
|
|
|
4
4
|
export const NAMESPACE = "registry-aggregator";
|
|
5
5
|
export const name = "registry-aggregator";
|
|
@@ -169,6 +169,14 @@ export function apply(ctx, config = {}) {
|
|
|
169
169
|
const value = { sources: await healthSources(sources, requestOptions), generatedAt: new Date().toISOString() };
|
|
170
170
|
return { ok: true, value: limitRpcValue(value, maxRpcBytes) };
|
|
171
171
|
}
|
|
172
|
+
if (endpoint === "counts") {
|
|
173
|
+
const sourceId = payload?.sourceId;
|
|
174
|
+
if (sourceId !== undefined && (typeof sourceId !== "string" || !sourceId.trim() || sourceId.length > 64)) throw new TypeError("sourceId must be a non-empty source id");
|
|
175
|
+
const selectedSources = sourceId === undefined ? sources : sources.filter((source) => source?.id === sourceId.trim());
|
|
176
|
+
if (sourceId !== undefined && selectedSources.length === 0) throw new RangeError("unknown sourceId");
|
|
177
|
+
const value = { sources: await countSources(selectedSources, requestOptions), generatedAt: new Date().toISOString() };
|
|
178
|
+
return { ok: true, value: limitRpcValue(value, maxRpcBytes) };
|
|
179
|
+
}
|
|
172
180
|
if (endpoint === "details") {
|
|
173
181
|
const request = detailsRequestFrom(payload);
|
|
174
182
|
const value = request.kind === "npm"
|
|
@@ -207,6 +215,7 @@ export function apply(ctx, config = {}) {
|
|
|
207
215
|
}
|
|
208
216
|
rpcCtx.connection.fetch.register(route("health"));
|
|
209
217
|
rpcCtx.connection.fetch.register(route("browse"));
|
|
218
|
+
rpcCtx.connection.fetch.register(route("counts"));
|
|
210
219
|
rpcCtx.connection.fetch.register(route("details"));
|
|
211
220
|
rpcCtx.connection.fetch.register(route("stars"));
|
|
212
221
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stolyarovmn/dsh-ui-registry-aggregator",
|
|
3
3
|
"description": "Federated plugin registry aggregator for DeepSeek Harness: combine trusted sources, search one index, and install through the native Plugin Manager.",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.15",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Stolyarovmn",
|
|
7
7
|
"url": "https://github.com/Stolyarovmn"
|