dshmarket 1.72.1 → 1.73.0
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 +1 -1
- package/README.zh.md +1 -1
- package/client/client.js +49 -11
- package/lib/discovery-compatibility.js +70 -9
- package/lib/routes.js +20 -3
- package/lib/types/discovery-compatibility.d.ts +8 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -57,7 +57,7 @@ its own dsh: it may be older than the one `npm` would give you (#139).
|
|
|
57
57
|
## What you get
|
|
58
58
|
|
|
59
59
|
- **Browse & search** the full community catalog (4200+ plugins, growing daily) — category filters, star counts, top/new sorting, bilingual descriptions that follow your UI language
|
|
60
|
-
- **Host-aware discovery** — cards show the DSH requirement declared by `engines.dsh` or lockstep `@deepseek-ai/dsh-*` peers;
|
|
60
|
+
- **Host-aware discovery** — cards show the DSH requirement declared by `engines.dsh` or lockstep `@deepseek-ai/dsh-*` peers; the compatibility filter hides only confirmed mismatches with the running host. GitHub-only entries read the linked package's manifest, including monorepo subdirectories; Release downloads read the selected release tag, including the current tag behind `latest/download`. Undeclared, malformed or unavailable requirements stay unknown and visible. Public manifest facts are cached; compatibility is evaluated against the running host
|
|
61
61
|
- **Screenshots** — AppStore-style screenshots, auto-carousel when there's more than one, click to preview full-size: author-curated shots show right on the card (zero extra requests); plugins without curated shots fall back to automatic README extraction once you open the install dialog. Images load from GitHub hosting only
|
|
62
62
|
- **Comments** — every card opens the plugin's discussion thread in place. It is the same thread its pages on [dshmarket.com](https://dshmarket.com) and the [catalog](https://awesome-dsh-plugin.com) show, so a plugin has one conversation rather than three. Backed by GitHub Discussions through giscus: it loads when you open it, needs a GitHub account only to post, and the note above it says plainly that opening it contacts giscus.app and GitHub. On local dsh web, reading stays embedded while a dedicated GitHub action opens the exact discussion in a new tab for sign-in and posting, so the cross-site return never carries or depends on the host session
|
|
63
63
|
- **Favorites** — bookmark plugins and themes from Discover or the Themes tab; a dedicated Favorites tab lists them with search, sort, and install actions. Bookmarks persist in the profile's market state (`state.json`); entries that leave the catalog can be cleared in one click
|
package/README.zh.md
CHANGED
|
@@ -50,7 +50,7 @@ profile,因此不会出现在桌面端。请通过应用内的 **设置 →
|
|
|
50
50
|
## 你会得到
|
|
51
51
|
|
|
52
52
|
- **逛与搜**——完整社区目录(4200+ 插件,每天在涨),分类筛选、star 数、最热/最新排序,中英描述跟随界面语言
|
|
53
|
-
- **按宿主发现**——卡片展示插件通过 `engines.dsh` 或同版本线 `@deepseek-ai/dsh-*` peer 声明的 DSH
|
|
53
|
+
- **按宿主发现**——卡片展示插件通过 `engines.dsh` 或同版本线 `@deepseek-ai/dsh-*` peer 声明的 DSH 要求;兼容筛选只隐藏与当前宿主明确不匹配的插件。仅 GitHub 发布的条目也会读取链接所指包的清单,支持子目录;Release 下载读取对应标签,`latest/download` 则读取当前发布标签。未声明、格式异常或暂时取不到清单时保持未知并继续显示。公开清单信息会缓存,兼容结论按当前宿主重新判断
|
|
54
54
|
- **截图展示**——App Store 式截图,多图自动轮播,点开还能看大图;作者在 registry 里策展的截图列表卡片就直接显示(零额外请求),没有策展的插件则在打开安装弹窗时自动从 README 抽取;图片仅从 GitHub 图床加载
|
|
55
55
|
- **评论**——每张卡片都能就地打开该插件的讨论。它和插件在 [dshmarket.com](https://dshmarket.com) 与[目录站](https://awesome-dsh-plugin.com)上的页面共用同一条讨论,一个插件只有一处对话,而不是三处。底层是 GitHub Discussions(经由 giscus):打开即加载,只有发表评论才需要 GitHub 账号;说明里也直说打开会连接 giscus.app 与 GitHub。本地 dsh web 继续内嵌评论供阅读,登录和发表则通过专用 GitHub 入口在新标签页打开当前插件的精确讨论,不让跨站回跳携带或依赖宿主会话
|
|
56
56
|
- **收藏**——在「发现」或「主题」页点书签即可收藏插件/主题;「收藏」Tab 集中展示,支持搜索、排序与安装。书签写入 profile 的市场状态(`state.json`);已下架条目可一键清除
|
package/client/client.js
CHANGED
|
@@ -2491,8 +2491,8 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
2491
2491
|
* Catalog matching for locally linked / file: installs. Shared by the host
|
|
2492
2492
|
* restore route and the Market client so both refuse the same wrong guesses.
|
|
2493
2493
|
*/
|
|
2494
|
-
const REPO_RE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
2495
|
-
function validSubpath(subpath) {
|
|
2494
|
+
const REPO_RE$1 = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
2495
|
+
function validSubpath$1(subpath) {
|
|
2496
2496
|
if (!/^[A-Za-z0-9_./-]+$/.test(subpath)) return false;
|
|
2497
2497
|
return !subpath.split("/").some((seg) => seg === "" || seg === "." || seg === "..");
|
|
2498
2498
|
}
|
|
@@ -2503,12 +2503,12 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
2503
2503
|
*/
|
|
2504
2504
|
function catalogMatchKeys(url) {
|
|
2505
2505
|
const m = /^https:\/\/github\.com\/([^/]+\/[^/]+?)(?:\/tree\/[^/]+\/(.+?))?\/?$/.exec(url);
|
|
2506
|
-
if (m === null || !REPO_RE.test(m[1])) return {
|
|
2506
|
+
if (m === null || !REPO_RE$1.test(m[1])) return {
|
|
2507
2507
|
path: null,
|
|
2508
2508
|
repo: null
|
|
2509
2509
|
};
|
|
2510
2510
|
const subpath = m[2] ?? null;
|
|
2511
|
-
if (subpath !== null && !validSubpath(subpath)) return {
|
|
2511
|
+
if (subpath !== null && !validSubpath$1(subpath)) return {
|
|
2512
2512
|
path: null,
|
|
2513
2513
|
repo: null
|
|
2514
2514
|
};
|
|
@@ -2652,6 +2652,34 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
2652
2652
|
};
|
|
2653
2653
|
}
|
|
2654
2654
|
//#endregion
|
|
2655
|
+
//#region src/sources.ts
|
|
2656
|
+
/**
|
|
2657
|
+
* Registry-source knowledge: how a curated registry entry's URL maps to an
|
|
2658
|
+
* installable pnpm target. Pure string logic, no I/O.
|
|
2659
|
+
*/
|
|
2660
|
+
const REPO_RE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
2661
|
+
function validSubpath(subpath) {
|
|
2662
|
+
if (!/^[A-Za-z0-9_./-]+$/.test(subpath)) return false;
|
|
2663
|
+
return !subpath.split("/").some((seg) => seg === "" || seg === "." || seg === "..");
|
|
2664
|
+
}
|
|
2665
|
+
/**
|
|
2666
|
+
* Parse a registry source url: a github repo, optionally with a
|
|
2667
|
+
* `/tree/<branch>/<subpath>` suffix (how the curated list links monorepo
|
|
2668
|
+
* subpackages, e.g. dsh-plugins#theme-gallery).
|
|
2669
|
+
*/
|
|
2670
|
+
function parseSourceUrl(url) {
|
|
2671
|
+
const m = /^https:\/\/github\.com\/([^/]+\/[^/]+?)(?:\/tree\/[^/]+\/(.+?))?\/?$/.exec(url);
|
|
2672
|
+
if (m === null || !REPO_RE.test(m[1])) return null;
|
|
2673
|
+
const subpath = m[2] ?? null;
|
|
2674
|
+
if (subpath !== null) {
|
|
2675
|
+
if (!validSubpath(subpath)) return null;
|
|
2676
|
+
}
|
|
2677
|
+
return {
|
|
2678
|
+
repo: m[1],
|
|
2679
|
+
subpath
|
|
2680
|
+
};
|
|
2681
|
+
}
|
|
2682
|
+
//#endregion
|
|
2655
2683
|
//#region src/client/market-data.ts
|
|
2656
2684
|
/** One registry entry from /dsh-market/registry. */
|
|
2657
2685
|
/**
|
|
@@ -2687,6 +2715,11 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
2687
2715
|
}
|
|
2688
2716
|
return categories;
|
|
2689
2717
|
}
|
|
2718
|
+
/** Keep GitHub packages in one repository distinct, independently of their display names. */
|
|
2719
|
+
function hostCompatibilityKey(plugin) {
|
|
2720
|
+
if (typeof plugin.npm === "string" && plugin.npm !== "") return plugin.npm;
|
|
2721
|
+
return parseSourceUrl(plugin.url) === null ? null : plugin.url;
|
|
2722
|
+
}
|
|
2690
2723
|
/**
|
|
2691
2724
|
* Whether a queued operation still applies — `null` when it does.
|
|
2692
2725
|
*
|
|
@@ -2931,7 +2964,8 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
2931
2964
|
const categories = pluginCategories(plugin);
|
|
2932
2965
|
if (options.category !== "all" && !categories.includes(options.category)) return [];
|
|
2933
2966
|
if (options.sinceDays !== void 0 && !withinDays(plugin.added, options.sinceDays)) return [];
|
|
2934
|
-
|
|
2967
|
+
const compatibilityKey = hostCompatibilityKey(plugin);
|
|
2968
|
+
if (options.compatibleWithHost === true && compatibilityKey !== null && options.hostCompatibility?.[compatibilityKey]?.status === "incompatible") return [];
|
|
2935
2969
|
const relevance = query === "" ? 0 : pluginRelevance(plugin, query, tokens, options.lang, options.categories);
|
|
2936
2970
|
return relevance === 0 && query !== "" ? [] : [{
|
|
2937
2971
|
plugin,
|
|
@@ -10515,7 +10549,10 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
10515
10549
|
const response = await fetch(api("/dsh-market/discovery-compatibility"), {
|
|
10516
10550
|
method: "POST",
|
|
10517
10551
|
headers: { "content-type": "application/json" },
|
|
10518
|
-
body: JSON.stringify({
|
|
10552
|
+
body: JSON.stringify({
|
|
10553
|
+
packages: chunk.filter((key) => !key.startsWith("https://")),
|
|
10554
|
+
...chunk.some((key) => key.startsWith("https://")) ? { entries: chunk.filter((key) => key.startsWith("https://")) } : {}
|
|
10555
|
+
})
|
|
10519
10556
|
});
|
|
10520
10557
|
const body = await response.json();
|
|
10521
10558
|
if (!response.ok || body.plugins === null || typeof body.plugins !== "object") throw new Error(`HTTP ${String(response.status)}`);
|
|
@@ -10934,8 +10971,8 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
10934
10971
|
compatibleWithHost
|
|
10935
10972
|
], scrollToTop);
|
|
10936
10973
|
const pagePlugins = plugins.slice((currentPage - 1) * pageSize, currentPage * pageSize);
|
|
10937
|
-
const pageHostPackages = [...new Set([...pagePlugins, ...recentPlugins].flatMap((plugin) =>
|
|
10938
|
-
const allHostPackages = (0, react.useMemo)(() => [...new Set((data?.plugins ?? []).flatMap((plugin) =>
|
|
10974
|
+
const pageHostPackages = [...new Set([...pagePlugins, ...recentPlugins].flatMap((plugin) => hostCompatibilityKey(plugin) === null ? [] : [hostCompatibilityKey(plugin)]))];
|
|
10975
|
+
const allHostPackages = (0, react.useMemo)(() => [...new Set((data?.plugins ?? []).flatMap((plugin) => hostCompatibilityKey(plugin) === null ? [] : [hostCompatibilityKey(plugin)]))], [data]);
|
|
10939
10976
|
const pageHostPackagesKey = pageHostPackages.join("\0");
|
|
10940
10977
|
const allHostPackagesKey = allHostPackages.join("\0");
|
|
10941
10978
|
(0, react.useEffect)(() => {
|
|
@@ -11028,7 +11065,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
11028
11065
|
const favoriteThemePagination = usePagination(favoriteThemes.length, favResetDeps, scrollToTop);
|
|
11029
11066
|
const favoritePagePlugins = favoritePlugins.slice((favoritePluginPagination.currentPage - 1) * favoritePluginPagination.pageSize, favoritePluginPagination.currentPage * favoritePluginPagination.pageSize);
|
|
11030
11067
|
const favoritePageThemes = favoriteThemes.slice((favoriteThemePagination.currentPage - 1) * favoriteThemePagination.pageSize, favoriteThemePagination.currentPage * favoriteThemePagination.pageSize);
|
|
11031
|
-
const favoritePageHostPackages = [...new Set(favoritePagePlugins.flatMap((plugin) =>
|
|
11068
|
+
const favoritePageHostPackages = [...new Set(favoritePagePlugins.flatMap((plugin) => hostCompatibilityKey(plugin) === null ? [] : [hostCompatibilityKey(plugin)]))];
|
|
11032
11069
|
const favoritePageHostPackagesKey = favoritePageHostPackages.join("\0");
|
|
11033
11070
|
(0, react.useEffect)(() => {
|
|
11034
11071
|
if (tab === "favorites") loadHostCompatibility(favoritePageHostPackages);
|
|
@@ -13161,8 +13198,9 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
13161
13198
|
const record = recordForUrl(records, p.url);
|
|
13162
13199
|
const blocked = record !== null && (record.state === "input" || record.state === "failed");
|
|
13163
13200
|
const queued = record !== null && record.state === "queued";
|
|
13164
|
-
const
|
|
13165
|
-
const
|
|
13201
|
+
const compatibilityKey = hostCompatibilityKey(p);
|
|
13202
|
+
const compatibility = compatibilityKey === null ? void 0 : hostCompatibility[compatibilityKey];
|
|
13203
|
+
const hostRequirementLabel = compatibility?.requirement !== null && compatibility?.requirement !== void 0 ? t("hostRequirement").replace("{0}", compatibility.requirement) : compatibilityKey === null ? t("hostRequirementUnavailable") : compatibility === void 0 ? t("hostRequirementLoading") : compatibility.basis === "undeclared" ? t("hostRequirementUndeclared") : t("hostRequirementUnavailable");
|
|
13166
13204
|
const hostRequirementTitle = compatibility?.declarations.map((declaration) => declaration.kind === "engine" ? `engines.dsh: ${declaration.range}` : `${declaration.package ?? "peer"}: ${declaration.range}`).join("\n") || hostRequirementLabel;
|
|
13167
13205
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
13168
13206
|
className: blocked ? `${Market_module_css_default.card} ${Market_module_css_default.cardBlocked}` : Market_module_css_default.card,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Discovery-time DSH compatibility metadata.
|
|
3
3
|
*
|
|
4
|
-
* The public catalog does not carry
|
|
4
|
+
* The public catalog does not carry package manifests. Fetching every manifest
|
|
5
5
|
* while the market opens would turn one catalog request into more than a
|
|
6
6
|
* thousand registry requests, so this module supplies a bounded, on-demand
|
|
7
7
|
* index. Successful public manifest facts are cached beside the market's
|
|
@@ -13,6 +13,7 @@ import { dirname } from 'node:path';
|
|
|
13
13
|
import { compareSemver, isSemver, satisfiesRange } from './check.js';
|
|
14
14
|
import { classifyPeer, crossesImplicitCeiling } from './compatibility.js';
|
|
15
15
|
import { marketFetch } from './net.js';
|
|
16
|
+
import { parseSourceUrl } from './sources.js';
|
|
16
17
|
const CACHE_SCHEMA = 'dsh-market/discovery-compatibility-cache/v1';
|
|
17
18
|
const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
|
|
18
19
|
const FAILURE_COOLDOWN_MS = 5 * 60 * 1000;
|
|
@@ -21,6 +22,44 @@ const FETCH_TIMEOUT_MS = 8_000;
|
|
|
21
22
|
const DEFAULT_CONCURRENCY = 8;
|
|
22
23
|
const MAX_CACHE_ENTRIES = 5_000;
|
|
23
24
|
const MAX_RANGE_LENGTH = 256;
|
|
25
|
+
/** Read the linked package at its selected release, never the repository root by accident. */
|
|
26
|
+
async function githubManifest(source, fetcher) {
|
|
27
|
+
const parsed = parseSourceUrl(source.url);
|
|
28
|
+
if (parsed === null)
|
|
29
|
+
throw new Error('not a GitHub package source');
|
|
30
|
+
const json = async (url) => {
|
|
31
|
+
const response = await fetcher(url, {
|
|
32
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
33
|
+
headers: { accept: 'application/json', 'user-agent': 'dsh-market' },
|
|
34
|
+
});
|
|
35
|
+
if (!response.ok)
|
|
36
|
+
throw new Error(`HTTP ${String(response.status)}`);
|
|
37
|
+
return await response.json();
|
|
38
|
+
};
|
|
39
|
+
let ref = /\/tree\/([^/]+)\//.exec(source.url)?.[1] ?? 'HEAD';
|
|
40
|
+
if (source.tarball != null) {
|
|
41
|
+
const release = /^https:\/\/github\.com\/([^/]+\/[^/]+)\/releases\/(?:download\/([^/]+)|latest\/download)\/[^/?#]+\.(?:tgz|tar\.gz)$/.exec(source.tarball);
|
|
42
|
+
if (release === null || release[1].toLowerCase() !== parsed.repo.toLowerCase()) {
|
|
43
|
+
throw new Error('archive does not name this repository release');
|
|
44
|
+
}
|
|
45
|
+
if (release[2] !== undefined)
|
|
46
|
+
ref = decodeURIComponent(release[2]);
|
|
47
|
+
else {
|
|
48
|
+
const latest = record(await json(`https://api.github.com/repos/${parsed.repo}/releases/latest`));
|
|
49
|
+
if (typeof latest?.tag_name !== 'string')
|
|
50
|
+
throw new Error('release tag unavailable');
|
|
51
|
+
ref = latest.tag_name;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (ref === '' || ref.length > 256 || ref.split('/').some(part => part === '' || part === '.' || part === '..')) {
|
|
55
|
+
throw new Error('invalid GitHub reference');
|
|
56
|
+
}
|
|
57
|
+
const file = parsed.subpath === null ? 'package.json' : `${parsed.subpath}/package.json`;
|
|
58
|
+
const manifest = await json(`https://raw.githubusercontent.com/${parsed.repo}/${encodeURIComponent(ref)}/${file}`);
|
|
59
|
+
if (record(manifest) === null)
|
|
60
|
+
throw new Error('package manifest unavailable');
|
|
61
|
+
return manifest;
|
|
62
|
+
}
|
|
24
63
|
function range(value) {
|
|
25
64
|
if (typeof value !== 'string')
|
|
26
65
|
return null;
|
|
@@ -140,7 +179,7 @@ export function deriveHostCompatibility(facts, hostVersion, hostPackages) {
|
|
|
140
179
|
return { status, basis: 'manifest', requirement, declarations };
|
|
141
180
|
}
|
|
142
181
|
/**
|
|
143
|
-
* Durable, bounded lookup of npm `latest` manifests.
|
|
182
|
+
* Durable, bounded lookup of npm `latest` and catalog GitHub manifests.
|
|
144
183
|
*
|
|
145
184
|
* Failed requests are intentionally memory-only and short-lived: a mirror
|
|
146
185
|
* outage must not become a day-long false "undeclared" result on disk.
|
|
@@ -212,7 +251,7 @@ export class DiscoveryManifestIndex {
|
|
|
212
251
|
this.fetchWaiters.shift()?.();
|
|
213
252
|
}
|
|
214
253
|
}
|
|
215
|
-
async fetchOne(name, registry, record = true) {
|
|
254
|
+
async fetchOne(name, registry, record = true, readManifest) {
|
|
216
255
|
this.load();
|
|
217
256
|
const now = this.now();
|
|
218
257
|
const cached = this.entries.get(name);
|
|
@@ -230,13 +269,16 @@ export class DiscoveryManifestIndex {
|
|
|
230
269
|
return await pending;
|
|
231
270
|
const request = (async () => {
|
|
232
271
|
try {
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
272
|
+
const value = await this.withFetchPermit(readManifest ?? (async () => {
|
|
273
|
+
const response = await this.fetcher(`${registry}/${encodeURIComponent(name)}/latest`, {
|
|
274
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
275
|
+
headers: { accept: 'application/json', 'user-agent': 'dsh-market' },
|
|
276
|
+
});
|
|
277
|
+
if (!response.ok)
|
|
278
|
+
throw new Error(`HTTP ${String(response.status)}`);
|
|
279
|
+
return await response.json();
|
|
236
280
|
}));
|
|
237
|
-
|
|
238
|
-
throw new Error(`HTTP ${String(response.status)}`);
|
|
239
|
-
const facts = manifestFacts(await response.json());
|
|
281
|
+
const facts = manifestFacts(value);
|
|
240
282
|
if (record) {
|
|
241
283
|
this.entries.set(name, { checkedAt: this.now(), facts });
|
|
242
284
|
this.dirty = true;
|
|
@@ -324,6 +366,25 @@ export class DiscoveryManifestIndex {
|
|
|
324
366
|
}
|
|
325
367
|
return result;
|
|
326
368
|
}
|
|
369
|
+
/** Catalog-approved GitHub sources, cached by both package path and selected archive. */
|
|
370
|
+
async lookupGitHub(sources) {
|
|
371
|
+
const unique = [...new Map(sources.map(source => [source.url, source])).values()];
|
|
372
|
+
const result = {};
|
|
373
|
+
let next = 0;
|
|
374
|
+
const worker = async () => {
|
|
375
|
+
while (next < unique.length) {
|
|
376
|
+
const source = unique[next++];
|
|
377
|
+
const key = `github:${source.url}\u0000${source.tarball ?? ''}`;
|
|
378
|
+
result[source.url] = await this.fetchOne(key, '', true, () => githubManifest(source, this.fetcher));
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
await Promise.all(Array.from({ length: Math.min(this.concurrency, unique.length) }, worker));
|
|
382
|
+
if (this.dirty) {
|
|
383
|
+
this.persist();
|
|
384
|
+
this.dirty = false;
|
|
385
|
+
}
|
|
386
|
+
return result;
|
|
387
|
+
}
|
|
327
388
|
}
|
|
328
389
|
/**
|
|
329
390
|
* The newest release of `npmName` whose own declarations this host satisfies.
|
package/lib/routes.js
CHANGED
|
@@ -33,7 +33,7 @@ import { applyPreset, deletePreset, listPresets, previewPreset, savePreset } fro
|
|
|
33
33
|
import { createProfileSnapshot, DEFAULT_MAX_SNAPSHOTS, deleteSnapshot, listSnapshots, restoreSnapshot } from './snapshot.js';
|
|
34
34
|
import { readHostRecoveryView, restoreHostRecoveryBackup } from './host-recovery.js';
|
|
35
35
|
import { trialValidate } from './trial.js';
|
|
36
|
-
import { catalogRepoKey, findCatalogEntryForLocal, findInstalledAlias, gitCommitOfTarget, githubCommitOfTarget, githubTargetAtCommit, gitAllowBuildsKey, gitRefOfTarget, gitTargetAtCommit, gitUpdateTarget, hostedRepoKey, lookupRepoFromUrl, pinnedAllowBuildsKeys, installTargetFor, isGenerationLink, isLocalSpec, NPM_NAME_RE, repoOfTarget, restoreBlockedByWorkspace, restoreTargetForLocal, sourceFallbackFor, workspaceProtocolDeps } from './sources.js';
|
|
36
|
+
import { catalogRepoKey, findCatalogEntryForLocal, findInstalledAlias, gitCommitOfTarget, githubCommitOfTarget, githubTargetAtCommit, gitAllowBuildsKey, gitRefOfTarget, gitTargetAtCommit, gitUpdateTarget, hostedRepoKey, lookupRepoFromUrl, pinnedAllowBuildsKeys, installTargetFor, isGenerationLink, isLocalSpec, NPM_NAME_RE, parseSourceUrl, repoOfTarget, restoreBlockedByWorkspace, restoreTargetForLocal, sourceFallbackFor, workspaceProtocolDeps } from './sources.js';
|
|
37
37
|
import { failureDetail, groupConflictsByOwner, installApplied, isHostPortCollision, isStaleUpdate, parseIgnoredBuildEntries, parseIgnoredBuilds, parsePrepareKey, parsePrepareNotAllowed, pnpmBlockedByOpenFiles, pnpmNeverStarted, pnpmRefusedYoungRelease, RELEASE_AGE_OVERRIDE, removeDanglingHostBridge, retargetCollections, updateApplied, validateAddedPlugins, withHoistRecovery } from './install.js';
|
|
38
38
|
import { classifyPnpmFailure } from './pnpm-compat.js';
|
|
39
39
|
import { asChannel, CHANNELS, DIST_TAG, resolveChannel } from './channels.js';
|
|
@@ -407,6 +407,7 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup, ho
|
|
|
407
407
|
const hostAnchorOption = config.dshInstallDir === undefined ? {} : { dshInstallDir: config.dshInstallDir };
|
|
408
408
|
const persistentLogFile = join(activeProfileDir, '.dsh-market', 'log.ndjson');
|
|
409
409
|
const discoveryManifests = new DiscoveryManifestIndex(join(activeProfileDir, '.dsh-market', 'discovery-compatibility-v1.json'));
|
|
410
|
+
const githubDiscoveryManifests = new DiscoveryManifestIndex(join(activeProfileDir, '.dsh-market', 'discovery-github-compatibility-v1.json'));
|
|
410
411
|
configurePersistentLog(persistentLogFile);
|
|
411
412
|
let agentGuardUnavailableLogged = false;
|
|
412
413
|
/** Running-agent ids for the mutation gate; logs once when the host exposes no agents service. */
|
|
@@ -2536,13 +2537,29 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup, ho
|
|
|
2536
2537
|
sendJson(response, 400, { error: 'packages must be an array of at most 64 npm package names' });
|
|
2537
2538
|
return;
|
|
2538
2539
|
}
|
|
2540
|
+
const requestedEntries = body.entries ?? [];
|
|
2541
|
+
if (!Array.isArray(requestedEntries) || requestedEntries.length + requested.length > 64
|
|
2542
|
+
|| !requestedEntries.every(url => typeof url === 'string' && parseSourceUrl(url) !== null)) {
|
|
2543
|
+
sendJson(response, 400, { error: 'entries must be GitHub catalog URLs; at most 64 packages and entries per batch' });
|
|
2544
|
+
return;
|
|
2545
|
+
}
|
|
2539
2546
|
const packages = [...new Set(requested)];
|
|
2540
2547
|
try {
|
|
2548
|
+
const urls = new Set(requestedEntries);
|
|
2549
|
+
const entries = urls.size === 0 ? [] : (await loadRegistry()).plugins.filter(entry => urls.has(entry.url) && (entry.npm == null || entry.npm === ''));
|
|
2550
|
+
if (new Set(entries.map(entry => entry.url)).size !== urls.size) {
|
|
2551
|
+
sendJson(response, 400, { error: 'entries must name GitHub-only plugins in the current catalog' });
|
|
2552
|
+
return;
|
|
2553
|
+
}
|
|
2541
2554
|
const host = dshHostInfo();
|
|
2542
2555
|
const hostVersion = host?.version ?? null;
|
|
2543
2556
|
const hostPackages = corePackageNames(host?.directory ?? null);
|
|
2544
|
-
const
|
|
2545
|
-
|
|
2557
|
+
const [npmFacts, githubFacts] = await Promise.all([
|
|
2558
|
+
discoveryManifests.lookup(packages, routesFor(region).npmRegistry),
|
|
2559
|
+
githubDiscoveryManifests.lookupGitHub(entries),
|
|
2560
|
+
]);
|
|
2561
|
+
const facts = { ...npmFacts, ...githubFacts };
|
|
2562
|
+
const plugins = Object.fromEntries([...packages, ...urls].map(name => [
|
|
2546
2563
|
name,
|
|
2547
2564
|
deriveHostCompatibility(facts[name] ?? null, hostVersion, hostPackages),
|
|
2548
2565
|
]));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Discovery-time DSH compatibility metadata.
|
|
3
3
|
*
|
|
4
|
-
* The public catalog does not carry
|
|
4
|
+
* The public catalog does not carry package manifests. Fetching every manifest
|
|
5
5
|
* while the market opens would turn one catalog request into more than a
|
|
6
6
|
* thousand registry requests, so this module supplies a bounded, on-demand
|
|
7
7
|
* index. Successful public manifest facts are cached beside the market's
|
|
@@ -32,6 +32,10 @@ type FetchLike = (url: string, init?: {
|
|
|
32
32
|
signal?: AbortSignal;
|
|
33
33
|
headers?: Record<string, string>;
|
|
34
34
|
}) => Promise<Response>;
|
|
35
|
+
export interface GitHubManifestSource {
|
|
36
|
+
url: string;
|
|
37
|
+
tarball?: string | null;
|
|
38
|
+
}
|
|
35
39
|
/** Keep only the small public subset of an npm manifest needed by discovery. */
|
|
36
40
|
export declare function manifestFacts(value: unknown): NpmManifestFacts;
|
|
37
41
|
/**
|
|
@@ -43,7 +47,7 @@ export declare function manifestFacts(value: unknown): NpmManifestFacts;
|
|
|
43
47
|
*/
|
|
44
48
|
export declare function deriveHostCompatibility(facts: NpmManifestFacts | null, hostVersion: string | null, hostPackages: ReadonlySet<string>): HostCompatibility;
|
|
45
49
|
/**
|
|
46
|
-
* Durable, bounded lookup of npm `latest` manifests.
|
|
50
|
+
* Durable, bounded lookup of npm `latest` and catalog GitHub manifests.
|
|
47
51
|
*
|
|
48
52
|
* Failed requests are intentionally memory-only and short-lived: a mirror
|
|
49
53
|
* outage must not become a day-long false "undeclared" result on disk.
|
|
@@ -107,6 +111,8 @@ export declare class DiscoveryManifestIndex {
|
|
|
107
111
|
lookup(names: readonly string[], registry: string, options?: {
|
|
108
112
|
record?: boolean;
|
|
109
113
|
}): Promise<Record<string, NpmManifestFacts | null>>;
|
|
114
|
+
/** Catalog-approved GitHub sources, cached by both package path and selected archive. */
|
|
115
|
+
lookupGitHub(sources: readonly GitHubManifestSource[]): Promise<Record<string, NpmManifestFacts | null>>;
|
|
110
116
|
}
|
|
111
117
|
/**
|
|
112
118
|
* The newest release of `npmName` whose own declarations this host satisfies.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dshmarket",
|
|
3
3
|
"description": "Visual plugin market inside DeepSeek Harness — browse, search, and one-click install community plugins. · DSH 可视化插件市场:逛一逛,点一下,装好。",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.73.0",
|
|
5
5
|
"icon": "assets/logo.svg",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "lib/index.js",
|