dshmarket 1.72.1 → 1.73.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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/profile.js +47 -2
- package/lib/routes.js +92 -7
- package/lib/types/discovery-compatibility.d.ts +8 -2
- package/lib/types/profile.d.ts +23 -0
- 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/profile.js
CHANGED
|
@@ -924,19 +924,64 @@ export function bundlePatchInsertedIds(dir) {
|
|
|
924
924
|
* conventional root cordis.patch.yml a package may ship instead.
|
|
925
925
|
*/
|
|
926
926
|
export function bundleOptOutRowIds(dir) {
|
|
927
|
+
return declaredPatchRowIds(dir, 'disabledIds');
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* Row ids a package's own patch sources declare for one role — the loop the
|
|
931
|
+
* two readers above and below share.
|
|
932
|
+
*
|
|
933
|
+
* They read the same two sources: every declared `dsh.bundle.patch` (one file,
|
|
934
|
+
* or an ordered list of them, #792) and the conventional root
|
|
935
|
+
* `cordis.patch.yml` a package may ship instead. Keeping that loop in one place
|
|
936
|
+
* is the point — `bundlePatchInsertedIds` above is declared-files-only, and a
|
|
937
|
+
* copy of this scan that forgot the root file is exactly how the two sides of
|
|
938
|
+
* one question drift apart.
|
|
939
|
+
*
|
|
940
|
+
* A source that cannot be read contributes nothing rather than throwing: this
|
|
941
|
+
* answers "what does the package say it ships", and a file that is not there
|
|
942
|
+
* says nothing. That is the tolerant half of the family by design — the strict
|
|
943
|
+
* one (`readBundlePatchRows`) refuses the whole declaration, because the host
|
|
944
|
+
* loads none of a patch it cannot read completely.
|
|
945
|
+
*/
|
|
946
|
+
function declaredPatchRowIds(dir, role) {
|
|
927
947
|
const ids = new Set();
|
|
928
948
|
const read = (file) => {
|
|
929
949
|
try {
|
|
930
|
-
for (const id of parsePatchRows(readFileSync(file, 'utf8'))
|
|
950
|
+
for (const id of parsePatchRows(readFileSync(file, 'utf8'))[role])
|
|
931
951
|
ids.add(id);
|
|
932
952
|
}
|
|
933
|
-
catch { /* no such patch — nothing
|
|
953
|
+
catch { /* no such patch — nothing declared through it */ }
|
|
934
954
|
};
|
|
935
955
|
for (const file of declaredBundlePatchFiles(dir))
|
|
936
956
|
read(file);
|
|
937
957
|
read(join(dir, 'cordis.patch.yml'));
|
|
938
958
|
return [...ids];
|
|
939
959
|
}
|
|
960
|
+
/**
|
|
961
|
+
* Row ids the installed copy of a package DECLARES it inserts (#1012).
|
|
962
|
+
*
|
|
963
|
+
* The evidence half of `rowIdsForPackage`: the same two patch sources (the
|
|
964
|
+
* declared `dsh.bundle.patch`, one file or an ordered list, and the
|
|
965
|
+
* conventional root `cordis.patch.yml`), insert rows only, and — unlike that
|
|
966
|
+
* reader — no live loader rows.
|
|
967
|
+
*
|
|
968
|
+
* Live rows are exactly what this cannot use. A rename transaction asks the
|
|
969
|
+
* question twice: of the old package just before it is removed (whose rows are
|
|
970
|
+
* still live, but whose patch files are about to be the only copy left) and of
|
|
971
|
+
* the new package right after its first install (which has never been booted,
|
|
972
|
+
* so the loader has never heard of it). Only the package's own declaration can
|
|
973
|
+
* answer for both.
|
|
974
|
+
*
|
|
975
|
+
* That is the same question, and the same parser, as `renamePredecessor` asks
|
|
976
|
+
* on the install path — an install never boots the package either, so it
|
|
977
|
+
* compares declarations too and the two refusals agree on what "one plugin"
|
|
978
|
+
* means (#1012). Where they differ is the root `cordis.patch.yml`: see
|
|
979
|
+
* `declaredPatchRowIds` for why this reader covers it and that gate's
|
|
980
|
+
* declared-only reader does not.
|
|
981
|
+
*/
|
|
982
|
+
export function shippedInsertedRowIds(dir) {
|
|
983
|
+
return declaredPatchRowIds(dir, 'insertedIds');
|
|
984
|
+
}
|
|
940
985
|
/**
|
|
941
986
|
* Rows of one patch file. Exported because a package may ship its patch at
|
|
942
987
|
* the conventional path INSTEAD of declaring `dsh.bundle.patch`, and the
|
package/lib/routes.js
CHANGED
|
@@ -24,7 +24,7 @@ import { diagnosePackageManifests } from './diagnostics.js';
|
|
|
24
24
|
import { BOOT_ID, cancelActive, probePnpm, progress, provisionPnpm, runDshPlugin, setBuildEnvSource, TARGET_RE, } from './dsh-cli.js';
|
|
25
25
|
import { packageOfEntryName } from './entry-identity.js';
|
|
26
26
|
import { PendingRowJournal, repairOrphanedRows } from './pending-rows.js';
|
|
27
|
-
import { addProfileBundle, bundleOptOutRowIds, bundlePatchInsertedIds, bundlesDroppedFromProfile, declareProfileDependency, dropFromManifest, generationManagedNames, hasLoadableEntry, holdsNativeAddon, INBOX_BUNDLES, isDshProfileName, profileDir, readDependencyOwners, readGitResolutionCommit, readInstalled, readInstalledManifest, readInstalledPackageName, readInstalledRepoEvidence, readInstalledVersion, readLockCommits, readProfileBundles, readProfileManifestSnapshot, removeProfileBundle, parsePatchRows, restoreProfileManifest, setAllowBuilds } from './profile.js';
|
|
27
|
+
import { addProfileBundle, bundleOptOutRowIds, bundlePatchInsertedIds, bundlesDroppedFromProfile, declareProfileDependency, dropFromManifest, generationManagedNames, hasLoadableEntry, holdsNativeAddon, INBOX_BUNDLES, isDshProfileName, profileDir, readDependencyOwners, readGitResolutionCommit, readInstalled, readInstalledManifest, readInstalledPackageName, readInstalledRepoEvidence, readInstalledVersion, readLockCommits, readProfileBundles, readProfileManifestSnapshot, removeProfileBundle, parsePatchRows, restoreProfileManifest, setAllowBuilds, shippedInsertedRowIds } from './profile.js';
|
|
28
28
|
import { assessProfile, classifyPeer, introducedDuplicateNames, introducedRisks } from './compatibility.js';
|
|
29
29
|
import { runningAgentIds } from './agents.js';
|
|
30
30
|
import { analyzeProfile, corePackageNames } from './check.js';
|
|
@@ -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
|
]));
|
|
@@ -4035,6 +4052,12 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup, ho
|
|
|
4035
4052
|
const wasLive = verifyActivation(config.profile, name, liveNames(), activeProfileDir, disabled.has(name)).state === 'live'
|
|
4036
4053
|
&& hasHostHalf(config.profile, name, activeProfileDir);
|
|
4037
4054
|
const oldRows = rowIdsForPackage(host, activeProfileDir, name);
|
|
4055
|
+
// Identity evidence for a rename (#1012), read BEFORE the removal:
|
|
4056
|
+
// once `remove` runs, the installed old package — and with it the
|
|
4057
|
+
// only record of what it registers — is gone.
|
|
4058
|
+
const oldInsertIds = kind === 'npm-rename'
|
|
4059
|
+
? shippedInsertedRowIds(join(activeProfileDir, 'node_modules', name))
|
|
4060
|
+
: [];
|
|
4038
4061
|
const patchFlags = packagePatchFlags(host, activeProfileDir, [name], readUserPatchState(userPatchPath));
|
|
4039
4062
|
const wasDisabled = disabled.has(name);
|
|
4040
4063
|
const manifestBefore = manifestCapture.snapshot;
|
|
@@ -4060,13 +4083,21 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup, ho
|
|
|
4060
4083
|
? { ok: true, detail: null }
|
|
4061
4084
|
: { ok: false, detail: `${restoredEn} was restored without a loadable entry` };
|
|
4062
4085
|
};
|
|
4063
|
-
|
|
4086
|
+
// `noteZh`/`noteEn` carry the "what you can do next" sentence, and
|
|
4087
|
+
// only the branch that actually restored the old install may say
|
|
4088
|
+
// it: after a failed rollback there is nothing to reassure the
|
|
4089
|
+
// user with, so the note is dropped rather than contradicting the
|
|
4090
|
+
// "could not be verified" fallout.
|
|
4091
|
+
const failWithRollback = async (detail, extra = {}, status = 500, noteZh = '', noteEn = '') => {
|
|
4064
4092
|
const rollback = await rollbackMigration();
|
|
4065
4093
|
logEvent(rollback.ok ? 'warn' : 'error', 'source-migration', `${name}: migration failed — ${detail}; ${rollback.ok ? `${restoredEn} restored` : `rollback failed: ${rollback.detail ?? 'unknown'}`}`);
|
|
4066
|
-
|
|
4094
|
+
// A 409 whose rollback failed is not a conflict the user can walk
|
|
4095
|
+
// away from — the install itself is now unverified — so the
|
|
4096
|
+
// status follows the rollback, not the reason we got here.
|
|
4097
|
+
sendJson(response, rollback.ok ? status : 500, {
|
|
4067
4098
|
ok: false,
|
|
4068
4099
|
error: rollback.ok
|
|
4069
|
-
? `${detail};已恢复${restoredZh}
|
|
4100
|
+
? `${detail};已恢复${restoredZh}。${noteZh === '' ? '' : ` ${noteZh}`} / ${detail}; ${restoredEn} was restored.${noteEn === '' ? '' : ` ${noteEn}`}`
|
|
4070
4101
|
: `${detail};且${restoredZh}未能验证恢复(${rollback.detail ?? 'unknown'})。 / ${detail}; restoration of ${restoredEn} could not be verified (${rollback.detail ?? 'unknown'}).`,
|
|
4071
4102
|
rollback: rollback.ok,
|
|
4072
4103
|
...extra,
|
|
@@ -4090,6 +4121,37 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup, ho
|
|
|
4090
4121
|
await failWithRollback('npm 目标安装完成后未形成可加载且唯一的依赖。 / The npm target did not produce one loadable replacement dependency.');
|
|
4091
4122
|
return;
|
|
4092
4123
|
}
|
|
4124
|
+
// The catalog's `previousNpm` is a CLAIM that the two package names
|
|
4125
|
+
// are one plugin, and the transaction above already acted on it by
|
|
4126
|
+
// replacing what the user had. Ask the packages themselves: the
|
|
4127
|
+
// rows each side declares it inserts are registry evidence, and two
|
|
4128
|
+
// packages that register disjoint rows are not the same plugin —
|
|
4129
|
+
// whatever the catalog says. Refuse and put the old one back rather
|
|
4130
|
+
// than leave a stranger's rows running under the old plugin's name
|
|
4131
|
+
// (#1012). Same question, same parser as the install-path gate
|
|
4132
|
+
// (`renamePredecessor`), which compares the installed package's
|
|
4133
|
+
// declared rows against the incoming registry `cordis.patch.yml`;
|
|
4134
|
+
// here both packages are already on disk, so both sides are read
|
|
4135
|
+
// the same way — including the conventional root patch file, which
|
|
4136
|
+
// that gate only knows on the incoming side.
|
|
4137
|
+
//
|
|
4138
|
+
// Both sides must SHOW evidence: a package with no patch file
|
|
4139
|
+
// declares nothing, and silence is not a contradiction.
|
|
4140
|
+
if (kind === 'npm-rename') {
|
|
4141
|
+
const newInsertIds = shippedInsertedRowIds(join(activeProfileDir, 'node_modules', targetName));
|
|
4142
|
+
if (oldInsertIds.length > 0 && newInsertIds.length > 0 && !sameLoaderRows(oldInsertIds, newInsertIds)) {
|
|
4143
|
+
// Both sides are third-party YAML: clip each id so a hostile
|
|
4144
|
+
// patch cannot turn this refusal into a megabyte response.
|
|
4145
|
+
const rows = (ids) => {
|
|
4146
|
+
const clip = (id) => (id.length <= 48 ? id : `${id.slice(0, 48)}…`);
|
|
4147
|
+
return ids.length <= 3
|
|
4148
|
+
? ids.map(clip).join(', ')
|
|
4149
|
+
: `${ids.slice(0, 3).map(clip).join(', ')}, +${ids.length - 3}`;
|
|
4150
|
+
};
|
|
4151
|
+
await failWithRollback(`目录声明 ${name} 已改名为 ${targetName},但两边的注册行不同(${name}: ${rows(oldInsertIds)};${targetName}: ${rows(newInsertIds)}),说明它们不是同一个插件。 / The catalog declares ${name} was renamed to ${targetName}, but the two packages register different plugin rows (${name}: ${rows(oldInsertIds)}; ${targetName}: ${rows(newInsertIds)}), so they are not the same plugin.`, {}, 409, `原来的版本仍然可用;如果目录写错了,请报告这一条。`, `The original install is still usable; if the catalog is wrong, report that entry.`);
|
|
4152
|
+
return;
|
|
4153
|
+
}
|
|
4154
|
+
}
|
|
4093
4155
|
const stack = readBundleStack(activeProfileDir);
|
|
4094
4156
|
const trial = trialValidate(activeProfileDir, stack.community, hostAnchorOption);
|
|
4095
4157
|
if (!trial.ok) {
|
|
@@ -5357,6 +5419,29 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup, ho
|
|
|
5357
5419
|
logEvent('warn', 'update-bundles-restored', `${name}: the desktop app removed its bundle row before refusing the update (bundle-in-use) — restored it so the plugin still loads on the next boot (#935)`);
|
|
5358
5420
|
}
|
|
5359
5421
|
}
|
|
5422
|
+
// A FAILED update can also drop rows it never intended to touch.
|
|
5423
|
+
// The rollback path ("restoration of the previous build could not
|
|
5424
|
+
// be verified") rewrites the manifest through pnpm and the plugin
|
|
5425
|
+
// command, and neither carries the bundle rows it did not own: a
|
|
5426
|
+
// bystander keeps its dependency and its node_modules but loses
|
|
5427
|
+
// its `dsh.profile.bundles` row, so it silently stops loading on
|
|
5428
|
+
// the next boot while this operation reported a failure about
|
|
5429
|
+
// something else entirely.
|
|
5430
|
+
//
|
|
5431
|
+
// Restoring is sound exactly because the operation FAILED: nobody
|
|
5432
|
+
// expressed a deliberate removal, and the package is still on
|
|
5433
|
+
// disk — putting the row back restores what the user had before
|
|
5434
|
+
// the operation. A successful update keeps the read-only stance
|
|
5435
|
+
// (#720/#696), where a missing row may be a deliberate switch-off.
|
|
5436
|
+
if (!ok && !cancelled) {
|
|
5437
|
+
const lostRows = bundlesDroppedFromProfile(manifestBefore, config.profile, activeProfileDir, new Set(removedDeclaration === null ? [] : [removedDeclaration.name])).filter(bundle => bundle !== name
|
|
5438
|
+
&& existsSync(join(activeProfileDir, 'node_modules', bundle, 'package.json')));
|
|
5439
|
+
for (const bundle of lostRows) {
|
|
5440
|
+
if (addProfileBundle(activeProfileDir, bundle)) {
|
|
5441
|
+
logEvent('warn', 'update-bundles-restored', `${bundle}: lost its dsh.profile.bundles row while ${name}'s update failed — restored it so the plugin still loads on the next boot (#720)`);
|
|
5442
|
+
}
|
|
5443
|
+
}
|
|
5444
|
+
}
|
|
5360
5445
|
logEvent(ok || cancelled ? 'info' : 'error', 'update', `${name} -> ${target} exit=${String(result.exitCode)}${result.timedOut ? ' TIMEOUT' : ''}${cancelled ? ' CANCELLED' : ''}${stale ? ` STALE(${staleReason ?? 'unknown'})` : ''}${ok || cancelled ? '' : ` err=${failureDetail(result)}`}`);
|
|
5361
5446
|
// A user-cancelled run is a quiet outcome, not an error.
|
|
5362
5447
|
sendJson(response, ok || cancelled ? 200 : result.busy === true ? 409 : 502, {
|
|
@@ -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/lib/types/profile.d.ts
CHANGED
|
@@ -324,6 +324,29 @@ export declare function bundlePatchInsertedIds(dir: string): string[];
|
|
|
324
324
|
* conventional root cordis.patch.yml a package may ship instead.
|
|
325
325
|
*/
|
|
326
326
|
export declare function bundleOptOutRowIds(dir: string): string[];
|
|
327
|
+
/**
|
|
328
|
+
* Row ids the installed copy of a package DECLARES it inserts (#1012).
|
|
329
|
+
*
|
|
330
|
+
* The evidence half of `rowIdsForPackage`: the same two patch sources (the
|
|
331
|
+
* declared `dsh.bundle.patch`, one file or an ordered list, and the
|
|
332
|
+
* conventional root `cordis.patch.yml`), insert rows only, and — unlike that
|
|
333
|
+
* reader — no live loader rows.
|
|
334
|
+
*
|
|
335
|
+
* Live rows are exactly what this cannot use. A rename transaction asks the
|
|
336
|
+
* question twice: of the old package just before it is removed (whose rows are
|
|
337
|
+
* still live, but whose patch files are about to be the only copy left) and of
|
|
338
|
+
* the new package right after its first install (which has never been booted,
|
|
339
|
+
* so the loader has never heard of it). Only the package's own declaration can
|
|
340
|
+
* answer for both.
|
|
341
|
+
*
|
|
342
|
+
* That is the same question, and the same parser, as `renamePredecessor` asks
|
|
343
|
+
* on the install path — an install never boots the package either, so it
|
|
344
|
+
* compares declarations too and the two refusals agree on what "one plugin"
|
|
345
|
+
* means (#1012). Where they differ is the root `cordis.patch.yml`: see
|
|
346
|
+
* `declaredPatchRowIds` for why this reader covers it and that gate's
|
|
347
|
+
* declared-only reader does not.
|
|
348
|
+
*/
|
|
349
|
+
export declare function shippedInsertedRowIds(dir: string): string[];
|
|
327
350
|
/**
|
|
328
351
|
* Rows of one patch file. Exported because a package may ship its patch at
|
|
329
352
|
* the conventional path INSTEAD of declaring `dsh.bundle.patch`, and the
|
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.1",
|
|
5
5
|
"icon": "assets/logo.svg",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "lib/index.js",
|