dshmarket 1.72.0 → 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.
Files changed (90) hide show
  1. package/README.md +1 -1
  2. package/README.zh.md +1 -1
  3. package/client/client.js +49 -11
  4. package/lib/discovery-compatibility.js +70 -9
  5. package/lib/profile.js +8 -3
  6. package/lib/routes.js +27 -4
  7. package/lib/types/discovery-compatibility.d.ts +8 -2
  8. package/package.json +1 -2
  9. package/src/accelerate.ts +0 -222
  10. package/src/agents.ts +0 -43
  11. package/src/backup.ts +0 -631
  12. package/src/catalog-local-match.ts +0 -185
  13. package/src/catalog-npm.ts +0 -152
  14. package/src/changelog.ts +0 -501
  15. package/src/channels.ts +0 -70
  16. package/src/check.ts +0 -1699
  17. package/src/client/CommentsModal.tsx +0 -119
  18. package/src/client/Diagnostics.tsx +0 -987
  19. package/src/client/ErrorBoundary.tsx +0 -111
  20. package/src/client/HostRecoveryBanner.tsx +0 -153
  21. package/src/client/InstallToast.tsx +0 -32
  22. package/src/client/Market.module.css +0 -1189
  23. package/src/client/MarketSection.tsx +0 -8615
  24. package/src/client/OperationsPanel.tsx +0 -416
  25. package/src/client/RecoveryPanel.tsx +0 -280
  26. package/src/client/SearchInput.tsx +0 -119
  27. package/src/client/SettingsCard.tsx +0 -759
  28. package/src/client/comments.ts +0 -54
  29. package/src/client/download-stats.ts +0 -30
  30. package/src/client/globals.d.ts +0 -31
  31. package/src/client/host-navigation.ts +0 -44
  32. package/src/client/icons.ts +0 -132
  33. package/src/client/index.ts +0 -440
  34. package/src/client/locales.ts +0 -2328
  35. package/src/client/market-data.ts +0 -1720
  36. package/src/client/market-element.ts +0 -80
  37. package/src/client/market-mark.ts +0 -60
  38. package/src/client/operations.ts +0 -382
  39. package/src/client/optional-primitives.ts +0 -90
  40. package/src/client/preset-panel.tsx +0 -263
  41. package/src/client/primitives.d.ts +0 -172
  42. package/src/client/section-gate.ts +0 -81
  43. package/src/client/self-check.ts +0 -147
  44. package/src/client/settings-nav-icon.ts +0 -174
  45. package/src/client/sidebar-entry.ts +0 -175
  46. package/src/client/snapshot-panel.tsx +0 -244
  47. package/src/compatibility.ts +0 -280
  48. package/src/diagnostics.ts +0 -84
  49. package/src/discovery-compatibility.ts +0 -444
  50. package/src/dsh-cli.ts +0 -1619
  51. package/src/dsh-install.ts +0 -207
  52. package/src/entry-identity.ts +0 -84
  53. package/src/gist.ts +0 -359
  54. package/src/groups.ts +0 -121
  55. package/src/home-paths.ts +0 -34
  56. package/src/host-recovery.ts +0 -208
  57. package/src/hot.ts +0 -1013
  58. package/src/http.ts +0 -271
  59. package/src/index.ts +0 -406
  60. package/src/install.ts +0 -1111
  61. package/src/lifecycle.ts +0 -35
  62. package/src/log-redaction.ts +0 -24
  63. package/src/log.ts +0 -205
  64. package/src/ndjson.ts +0 -185
  65. package/src/net.ts +0 -153
  66. package/src/official-desktop.ts +0 -624
  67. package/src/order.ts +0 -303
  68. package/src/patch.ts +0 -700
  69. package/src/pending-rows.ts +0 -178
  70. package/src/pending-updates.ts +0 -356
  71. package/src/pnpm-compat.ts +0 -908
  72. package/src/presets.ts +0 -355
  73. package/src/profile.ts +0 -1829
  74. package/src/recovery.ts +0 -1205
  75. package/src/region-probe.ts +0 -97
  76. package/src/regions.ts +0 -367
  77. package/src/registry.ts +0 -532
  78. package/src/release-candidates.ts +0 -115
  79. package/src/restart.ts +0 -723
  80. package/src/routes.ts +0 -7185
  81. package/src/settings.ts +0 -214
  82. package/src/snapshot.ts +0 -532
  83. package/src/source-migration.ts +0 -195
  84. package/src/sources.ts +0 -984
  85. package/src/store.ts +0 -155
  86. package/src/themes.ts +0 -333
  87. package/src/trial.ts +0 -163
  88. package/src/update-api-v1.ts +0 -302
  89. package/src/updates.ts +0 -981
  90. package/src/verify.ts +0 -1009
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; an opt-in filter hides only confirmed mismatches with the running host. Undeclared, malformed, unavailable, and GitHub-only entries remain visible rather than being guessed incompatible
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 要求;可选筛选只隐藏与当前宿主明确不匹配的插件。未声明、格式异常、暂时取不到清单及仅 GitHub 发布的条目仍保持可见,不猜成不兼容
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
- if (options.compatibleWithHost === true && plugin.npm != null && options.hostCompatibility?.[plugin.npm]?.status === "incompatible") return [];
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({ packages: chunk })
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) => typeof plugin.npm === "string" && plugin.npm !== "" ? [plugin.npm] : []))];
10938
- const allHostPackages = (0, react.useMemo)(() => [...new Set((data?.plugins ?? []).flatMap((plugin) => typeof plugin.npm === "string" && plugin.npm !== "" ? [plugin.npm] : []))], [data]);
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) => typeof plugin.npm === "string" && plugin.npm !== "" ? [plugin.npm] : []))];
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 compatibility = typeof p.npm === "string" ? hostCompatibility[p.npm] : void 0;
13165
- const hostRequirementLabel = compatibility?.requirement !== null && compatibility?.requirement !== void 0 ? t("hostRequirement").replace("{0}", compatibility.requirement) : typeof p.npm !== "string" ? t("hostRequirementUnavailable") : compatibility === void 0 ? t("hostRequirementLoading") : compatibility.basis === "undeclared" ? t("hostRequirementUndeclared") : t("hostRequirementUnavailable");
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 npm manifests. Fetching every manifest
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 response = await this.withFetchPermit(async () => await this.fetcher(`${registry}/${encodeURIComponent(name)}/latest`, {
234
- signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
235
- headers: { accept: 'application/json', 'user-agent': 'dsh-market' },
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
- if (!response.ok)
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/profile.js CHANGED
@@ -1491,6 +1491,11 @@ export function setAllowBuilds(profile, packages, explicitDir) {
1491
1491
  const SEG = '[A-Za-z0-9_-][A-Za-z0-9_.-]*';
1492
1492
  const NAME = '[A-Za-z0-9@/_.-]+';
1493
1493
  const SHA = '[0-9a-f]{40}';
1494
+ // A dependency pnpm resolved from a BRANCH keys its decision by the ref,
1495
+ // not by a commit: `name@https://codeload.github.com/o/r/tar.gz/refs/heads/main`
1496
+ // (#1034). The ref is path segments only — no `.`/`..`, no empty piece —
1497
+ // so it cannot walk out of the URL it is written into.
1498
+ const REF = `refs/(?:heads|tags)/${SEG}(?:/${SEG})*`;
1494
1499
  // pnpm's exact monorepo key includes the selected directory; dropping it
1495
1500
  // makes approve-and-retry write only a bare name that git builds ignore.
1496
1501
  const DIR = '(?!\\.{1,2}(?:/|$))[A-Za-z0-9_.-]+';
@@ -1512,9 +1517,9 @@ export function setAllowBuilds(profile, packages, explicitDir) {
1512
1517
  // gitlab.com and bitbucket.org (#637). Each branch names its host and its
1513
1518
  // exact path shape.
1514
1519
  const ARCHIVE_KEY_RE = new RegExp(`^${NAME}@https://(?:`
1515
- + `codeload\\.github\\.com/${SEG}/${SEG}/tar\\.gz/${SHA}`
1516
- + `|bitbucket\\.org/${SEG}/${SEG}/get/${SHA}\\.tar\\.gz`
1517
- + `|gitlab\\.com/${SEG}(?:/${SEG})+/-/archive/${SHA}/${SEG}-${SHA}\\.tar\\.gz`
1520
+ + `codeload\\.github\\.com/${SEG}/${SEG}/tar\\.gz/(?:${SHA}|${REF})`
1521
+ + `|bitbucket\\.org/${SEG}/${SEG}/get/(?:${SHA}|${REF})\\.tar\\.gz`
1522
+ + `|gitlab\\.com/${SEG}(?:/${SEG})+/-/archive/(?:${SHA}|${REF})/${SEG}-(?:${SHA}|${REF})\\.tar\\.gz`
1518
1523
  + `)(?:#${SUBPATH})?$`);
1519
1524
  for (const pkg of packages) {
1520
1525
  if (/^[A-Za-z0-9@/_.-]+$/.test(pkg) || GIT_KEY_RE.test(pkg) || ARCHIVE_KEY_RE.test(pkg))
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 facts = await discoveryManifests.lookup(packages, routesFor(region).npmRegistry);
2545
- const plugins = Object.fromEntries(packages.map(name => [
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
  ]));
@@ -6923,7 +6940,13 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup, ho
6923
6940
  // where the button is not (#314). The panel now carries
6924
6941
  // the action on this very row, so the text can just say
6925
6942
  // what to press.
6926
- ? `构建脚本被 pnpm 默认拦截(${ignoredBuilds.join(', ')}),点击「放行构建脚本并重试」即可放行并重装 / build scripts are blocked by pnpm by default (${ignoredBuilds.join(', ')}); use "Allow build scripts and retry" to approve and reinstall`
6943
+ // #1034: pnpm reports every undecided build in the
6944
+ // workspace, not only the ones this operation needed — an
6945
+ // entry left from an earlier install shows up here too, and
6946
+ // saying "this install needs dsh-purge approved" was wrong
6947
+ // for a plugin that never depended on it. Name them as
6948
+ // decisions, with where the decision is written.
6949
+ ? `pnpm 要求先决定这些依赖能否运行构建脚本(${ignoredBuilds.join(', ')}):有的是这次安装带进来的,也可能是之前留下的未决定条目。点「放行构建脚本并重试」会把它们写进 pnpm-workspace.yaml 的 allowBuilds,然后重新安装 / pnpm wants a decision on these packages before it runs their build scripts (${ignoredBuilds.join(', ')}); some came in with this install, and some may be entries an earlier one left undecided. "Allow build scripts and retry" writes them into allowBuilds in pnpm-workspace.yaml and installs again`
6927
6950
  // Same last resort as the update route (#848), and on THIS
6928
6951
  // route too: a recognized failure like an expired
6929
6952
  // `patchedDependencies` key had no branch here either.
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Discovery-time DSH compatibility metadata.
3
3
  *
4
- * The public catalog does not carry npm manifests. Fetching every manifest
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.72.0",
4
+ "version": "1.73.0",
5
5
  "icon": "assets/logo.svg",
6
6
  "type": "module",
7
7
  "main": "lib/index.js",
@@ -76,7 +76,6 @@
76
76
  "assets/logo.svg",
77
77
  "locale",
78
78
  "lib",
79
- "src",
80
79
  "client",
81
80
  "UPDATE-API-V1.md",
82
81
  "cordis.patch.yml",