dshmarket 1.66.43 → 1.66.45
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/client/client.js +21 -9
- package/lib/check.js +19 -7
- package/lib/patch.js +48 -18
- package/lib/profile.js +7 -3
- package/lib/routes.js +27 -11
- package/lib/trial.js +1 -1
- package/lib/types/check.d.ts +8 -3
- package/lib/types/patch.d.ts +17 -12
- package/lib/types/profile.d.ts +6 -2
- package/lib/types/updates.d.ts +23 -1
- package/lib/updates.js +79 -7
- package/package.json +1 -1
- package/src/check.ts +19 -7
- package/src/client/MarketSection.tsx +17 -6
- package/src/client/locales.ts +4 -0
- package/src/client/market-data.ts +20 -7
- package/src/patch.ts +45 -17
- package/src/profile.ts +12 -4
- package/src/routes.ts +29 -12
- package/src/trial.ts +1 -1
- package/src/updates.ts +85 -6
package/client/client.js
CHANGED
|
@@ -393,6 +393,8 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
393
393
|
updateNow: "立即更新",
|
|
394
394
|
updateFail: "更新失败",
|
|
395
395
|
upToDate: "已是最新",
|
|
396
|
+
updateHeldTag: "新版等待中",
|
|
397
|
+
updateHeldHint: "这个 profile 要求新版本发布满一段时间才允许安装,大约还要 {1} 小时。",
|
|
396
398
|
newerPrereleaseAvailable: "预发布版 {0} 已发布",
|
|
397
399
|
newerPrereleaseHint: "npm 上的 latest 没有更新;你在跟的这条预发布线发布了更新的版本。",
|
|
398
400
|
unreleasedCommits: "有新提交(未发版){0}",
|
|
@@ -1132,6 +1134,8 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
1132
1134
|
updateNow: "Update now",
|
|
1133
1135
|
updateFail: "Update failed",
|
|
1134
1136
|
upToDate: "Up to date",
|
|
1137
|
+
updateHeldTag: "new version waiting",
|
|
1138
|
+
updateHeldHint: "This profile waits a while before it installs a fresh release; about {1} hour(s) left.",
|
|
1135
1139
|
newerPrereleaseAvailable: "prerelease {0} published",
|
|
1136
1140
|
newerPrereleaseHint: "The package's latest tag has no update; the prerelease line you are on published a newer build.",
|
|
1137
1141
|
unreleasedCommits: "New commits (unreleased) {0}",
|
|
@@ -1993,8 +1997,9 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
1993
1997
|
}
|
|
1994
1998
|
/** Heuristic: plugins that target a terminal surface rather than the web UI. */
|
|
1995
1999
|
function looksTerminal(plugin, lang) {
|
|
2000
|
+
if (/\b(tui|cli|tty|terminal)\b|终端|命令行/i.test(plugin.name)) return true;
|
|
1996
2001
|
const positiveDesc = (plugin.description && (plugin.description[lang] || plugin.description.en) || "").replace(/\b(?:no|without)\b[^.!?;:,。!?;\n]{0,80}\b(?:tui|cli|tty|terminal)\b/gi, "").replace(/(?:无需|无须|不需要|不用)[^。!?;\n]{0,48}(?:tui|cli|tty|terminal|终端|命令行)/gi, "");
|
|
1997
|
-
return /\b(
|
|
2002
|
+
return /\b(?:terminal|cli)[ -]only\b|\btui (?:app|plugin|client)\b|\bterminal (?:ui|app|plugin|client)\b|\bcommand[- ]line (?:tool|interface|app|client|plugin)\b|命令行(?:工具|插件|应用|界面|客户端)|终端(?:工具|插件|应用|界面|客户端)|只在终端|仅在终端|需要(?:一个)?终端|在终端(?:里|中)运行/i.test(positiveDesc);
|
|
1998
2003
|
}
|
|
1999
2004
|
/** Days per TimeRange (`all` has no cutoff and is handled by the caller). */
|
|
2000
2005
|
const TIME_RANGE_DAYS = {
|
|
@@ -2891,14 +2896,17 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
2891
2896
|
*
|
|
2892
2897
|
* The repository name IS the plugin name in the ordinary case, because a
|
|
2893
2898
|
* repository holding one plugin is named after it. Only the compound form
|
|
2894
|
-
* needs unpicking
|
|
2899
|
+
* needs unpicking. Prefer its mapped npm package when available: a generic
|
|
2900
|
+
* directory such as `plugin` is not the product name. Without that mapping,
|
|
2901
|
+
* keep the directory-leaf fallback for source-only entries.
|
|
2895
2902
|
*
|
|
2896
2903
|
* Not a substitute for the identity: every key, lookup and install still
|
|
2897
2904
|
* uses `name` unchanged.
|
|
2898
2905
|
*/
|
|
2899
|
-
function pluginName(name) {
|
|
2906
|
+
function pluginName(name, npm) {
|
|
2900
2907
|
const hash = name.indexOf("#");
|
|
2901
2908
|
if (hash === -1) return name;
|
|
2909
|
+
if (typeof npm === "string" && npm.trim() !== "") return npm.trim();
|
|
2902
2910
|
const sub = name.slice(hash + 1);
|
|
2903
2911
|
const leaf = sub.slice(sub.lastIndexOf("/") + 1);
|
|
2904
2912
|
return leaf === "" ? name.slice(0, hash) : leaf;
|
|
@@ -7391,7 +7399,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
7391
7399
|
plugin.url
|
|
7392
7400
|
]);
|
|
7393
7401
|
const visible = (curated.length > 0 ? curated : fallback.shots).filter((src) => !broken.includes(src));
|
|
7394
|
-
const name = pluginName(plugin.name);
|
|
7402
|
+
const name = pluginName(plugin.name, plugin.npm);
|
|
7395
7403
|
if (visible.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
7396
7404
|
ref: setCoverRef,
|
|
7397
7405
|
type: "button",
|
|
@@ -8698,7 +8706,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
8698
8706
|
const entry = data?.plugins.find((plugin) => plugin.npm === name || plugin.name === name);
|
|
8699
8707
|
if (entry === void 0) return { title: name };
|
|
8700
8708
|
return {
|
|
8701
|
-
title: pluginName(entry.name),
|
|
8709
|
+
title: pluginName(entry.name, entry.npm),
|
|
8702
8710
|
author: entry.owner === "" ? void 0 : entry.owner,
|
|
8703
8711
|
avatar: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OwnerAvatar, {
|
|
8704
8712
|
name: entry.name,
|
|
@@ -11812,7 +11820,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
11812
11820
|
title: p.name,
|
|
11813
11821
|
"aria-label": `${p.name} — ${t("repoLink")}`,
|
|
11814
11822
|
children: [
|
|
11815
|
-
pluginName(p.name),
|
|
11823
|
+
pluginName(p.name, p.npm),
|
|
11816
11824
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(GithubRepoMark, { className: Market_module_css_default.repoMark }),
|
|
11817
11825
|
p.deprecated === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
11818
11826
|
className: Market_module_css_default.depBadge,
|
|
@@ -12001,7 +12009,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
12001
12009
|
rel: "noreferrer",
|
|
12002
12010
|
title: p.name,
|
|
12003
12011
|
"aria-label": `${p.name} — ${t("repoLink")}`,
|
|
12004
|
-
children: [pluginName(p.name), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GithubRepoMark, { className: Market_module_css_default.repoMark })]
|
|
12012
|
+
children: [pluginName(p.name, p.npm), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GithubRepoMark, { className: Market_module_css_default.repoMark })]
|
|
12005
12013
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
12006
12014
|
className: Market_module_css_default.byline,
|
|
12007
12015
|
children: [
|
|
@@ -14150,6 +14158,10 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
14150
14158
|
else doUpdate(name);
|
|
14151
14159
|
},
|
|
14152
14160
|
children: status.restoreRequired === true ? t("restoreOnline") : t("update")
|
|
14161
|
+
}) : status?.heldUpdate !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
14162
|
+
className: Market_module_css_default.metaTag,
|
|
14163
|
+
title: t("updateHeldHint").replace("{1}", String(Math.max(1, Math.ceil((Date.parse(status.heldUpdate.until) - Date.now()) / 36e5)))),
|
|
14164
|
+
children: t("updateHeldTag").replace("{0}", displayLatest(status.heldUpdate.latest))
|
|
14153
14165
|
}) : localDev ? null : status?.unreleasedCommit != null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
14154
14166
|
className: Market_module_css_default.metaTag,
|
|
14155
14167
|
title: t("unreleasedCommitsHint"),
|
|
@@ -14381,7 +14393,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
14381
14393
|
className: Market_module_css_default.confirmModal,
|
|
14382
14394
|
contentClassName: Market_module_css_default.confirmContent,
|
|
14383
14395
|
onClose: () => setConfirming(null),
|
|
14384
|
-
title: t("confirmTitle") + " " + confirming.name + "?",
|
|
14396
|
+
title: t("confirmTitle") + " " + pluginName(confirming.name, confirming.npm) + "?",
|
|
14385
14397
|
footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
14386
14398
|
variant: "ghost",
|
|
14387
14399
|
onClick: () => setConfirming(null),
|
|
@@ -14537,7 +14549,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
14537
14549
|
t
|
|
14538
14550
|
}),
|
|
14539
14551
|
commentsFor !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CommentsModal, {
|
|
14540
|
-
name: pluginName(commentsFor.name),
|
|
14552
|
+
name: pluginName(commentsFor.name, commentsFor.npm),
|
|
14541
14553
|
url: commentsFor.url,
|
|
14542
14554
|
lang,
|
|
14543
14555
|
onClose: () => setCommentsFor(null),
|
package/lib/check.js
CHANGED
|
@@ -761,15 +761,27 @@ export function composeLayers(layers) {
|
|
|
761
761
|
* patch row plus a user-patch row on the same id: fatal forever meant every
|
|
762
762
|
* update of every plugin rolled back.
|
|
763
763
|
*
|
|
764
|
-
*
|
|
765
|
-
* the
|
|
764
|
+
* An empty answer from the PROFILE lockfile is not "unknown" on the official
|
|
765
|
+
* Desktop: that host embeds the loader in `app.asar` and provides it to every
|
|
766
|
+
* profile, so no profile can ever name it (#943). Reading that as an old
|
|
767
|
+
* loader made the gate fatal for every update on 0.2.1-rc.2 — the exact report
|
|
768
|
+
* #853's fix was supposed to close. The host installation is asked before the
|
|
769
|
+
* evidence is treated as missing; only when both are silent does the
|
|
770
|
+
* conservative answer stand.
|
|
766
771
|
*/
|
|
767
|
-
export function duplicateEntryIdsAreFatal(profileDirectory) {
|
|
772
|
+
export function duplicateEntryIdsAreFatal(profileDirectory, dshInstallDir) {
|
|
768
773
|
const versions = lockfileCoreVersions(profileDirectory).get('@deepseek-ai/cordis-plugin-loader') ?? [];
|
|
769
|
-
if (versions.length
|
|
774
|
+
if (versions.length > 0) {
|
|
775
|
+
const newest = versions.reduce((a, b) => (compareSemver(a, b) >= 0 ? a : b));
|
|
776
|
+
return compareSemver(newest, '1.0.4') < 0;
|
|
777
|
+
}
|
|
778
|
+
const install = dshInstallDir === undefined ? findDshInstallDir() : dshInstallDir;
|
|
779
|
+
const host = install === null
|
|
780
|
+
? null
|
|
781
|
+
: readNodeModulesVersion(dirname(hostNodeModulesRoot(install)), '@deepseek-ai/cordis-plugin-loader');
|
|
782
|
+
if (host === null)
|
|
770
783
|
return true;
|
|
771
|
-
|
|
772
|
-
return compareSemver(newest, '1.0.4') < 0;
|
|
784
|
+
return compareSemver(host, '1.0.4') < 0;
|
|
773
785
|
}
|
|
774
786
|
/** Distinct versions of `@deepseek-ai/{dsh,cordis}*` packages in the lockfile. */
|
|
775
787
|
function lockfileCoreVersions(profileDir) {
|
|
@@ -1349,7 +1361,7 @@ export function analyzeProfile(profileDirectory, options = {}) {
|
|
|
1349
1361
|
// Rows on one id: a boot failure on an old loader, a silent override on a
|
|
1350
1362
|
// new one (see duplicateEntryIdsAreFatal). Refusing an operation over the
|
|
1351
1363
|
// second shape rolled back every update for the reporter's profile.
|
|
1352
|
-
const duplicateFatal = duplicateEntryIdsAreFatal(profileDirectory);
|
|
1364
|
+
const duplicateFatal = duplicateEntryIdsAreFatal(profileDirectory, dshInstall);
|
|
1353
1365
|
for (const dup of composed.duplicates) {
|
|
1354
1366
|
if (duplicateFatal) {
|
|
1355
1367
|
errors.push(`duplicate loader entry id ${JSON.stringify(dup.id)} (${dup.count} rows: ${dup.layers.join(', ')})`);
|
package/lib/patch.js
CHANGED
|
@@ -319,40 +319,70 @@ function foreignDisableIds(rows) {
|
|
|
319
319
|
return ids;
|
|
320
320
|
}
|
|
321
321
|
/**
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
322
|
+
* Top-level patch rows that add an `inject:` requirement to another plugin.
|
|
323
|
+
*
|
|
324
|
+
* Same shape rule as foreignDisableIds: anything at the top level with an id
|
|
325
|
+
* is a statement about a plugin this package does not own. An `inject` there
|
|
326
|
+
* makes that foreign row depend on a service — usually one this bundle
|
|
327
|
+
* provides — so switching this package off while leaving its patch in the
|
|
328
|
+
* stack strands the row: #938's billion-context adds
|
|
329
|
+
* `inject: [biliBasicCompactionPolicy]` to the four official agent presets,
|
|
330
|
+
* and a row-level disable left them unable to activate.
|
|
331
|
+
*/
|
|
332
|
+
function foreignInjectIds(rows) {
|
|
333
|
+
const ids = [];
|
|
334
|
+
for (const row of rows) {
|
|
335
|
+
if (row === null || typeof row !== 'object' || Array.isArray(row))
|
|
336
|
+
continue;
|
|
337
|
+
const record = row;
|
|
338
|
+
const id = record.id;
|
|
339
|
+
if (typeof id === 'string' && record.inject !== undefined && !ids.includes(id))
|
|
340
|
+
ids.push(id);
|
|
341
|
+
}
|
|
342
|
+
return ids;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* The ids of OTHER plugins a package's bundle patch rewires — top-level rows
|
|
346
|
+
* that either `disabled: true` a plugin it does not own, or add an `inject:`
|
|
347
|
+
* requirement to one.
|
|
348
|
+
*
|
|
349
|
+
* Both are the marker of a bundle whose toggle-off can brick the boot while
|
|
350
|
+
* its patch keeps applying (#224, #938):
|
|
351
|
+
* - dsh-postgres-backends disables session-persistence-jsonl, so once the
|
|
352
|
+
* market also disables the postgres backends nothing provides
|
|
353
|
+
* sessionPersistence;
|
|
354
|
+
* - billion-context adds `inject: [biliBasicCompactionPolicy]` to the four
|
|
355
|
+
* official agent presets, so disabling only its own row leaves the presets
|
|
356
|
+
* waiting for a service that no longer exists — every preset disappears.
|
|
327
357
|
*
|
|
328
358
|
* A bundle that merely RECONFIGURES a neighbour is deliberately NOT counted:
|
|
329
359
|
* the e2e fixture-cross tweaks dshm-fixture-b's config, and #147 requires
|
|
330
360
|
* disabling it to leave that neighbour live — dropping such a bundle from the
|
|
331
361
|
* stack broke its re-enable. Config-only side effects stay on the normal #147
|
|
332
|
-
* path
|
|
333
|
-
*
|
|
334
|
-
* whole patch stops applying.
|
|
362
|
+
* path. Removing the bundle still neutralizes any config side effects it
|
|
363
|
+
* carries, since its whole patch stops applying.
|
|
335
364
|
*
|
|
336
365
|
* Reads both patch sources like rowIdsForPackage — the declared
|
|
337
366
|
* dsh.bundle.patch (one file or a LIST of them, #792) and the conventional
|
|
338
367
|
* root cordis.patch.yml — so either form is detected, and every declared file
|
|
339
|
-
* is attributed
|
|
340
|
-
* bricks the boot, #224).
|
|
368
|
+
* is attributed.
|
|
341
369
|
*/
|
|
342
|
-
export function
|
|
370
|
+
export function carrierRowIds(profileDirectory, packageName) {
|
|
343
371
|
const packageDir = join(profileDirectory, 'node_modules', packageName);
|
|
344
|
-
const
|
|
372
|
+
const rows = new Set();
|
|
345
373
|
const collectFromFile = (patchPath) => {
|
|
346
|
-
const
|
|
347
|
-
if (
|
|
374
|
+
const parsed = parsePatchFile(patchPath);
|
|
375
|
+
if (parsed === null)
|
|
348
376
|
return;
|
|
349
|
-
for (const id of foreignDisableIds(
|
|
350
|
-
|
|
377
|
+
for (const id of foreignDisableIds(parsed))
|
|
378
|
+
rows.add(id);
|
|
379
|
+
for (const id of foreignInjectIds(parsed))
|
|
380
|
+
rows.add(id);
|
|
351
381
|
};
|
|
352
382
|
for (const file of declaredBundlePatchFiles(packageDir))
|
|
353
383
|
collectFromFile(file);
|
|
354
384
|
collectFromFile(join(packageDir, 'cordis.patch.yml'));
|
|
355
|
-
return [...
|
|
385
|
+
return [...rows];
|
|
356
386
|
}
|
|
357
387
|
/**
|
|
358
388
|
* The ids a package's bundle patch touches WITHOUT inserting them itself.
|
|
@@ -369,7 +399,7 @@ export function carrierDisableIds(profileDirectory, packageName) {
|
|
|
369
399
|
* fixture-cross e2e exist to protect, where the neighbour must keep running
|
|
370
400
|
* and the bundle must keep re-enabling.
|
|
371
401
|
*
|
|
372
|
-
* Reads both patch sources like
|
|
402
|
+
* Reads both patch sources like carrierRowIds: the declared
|
|
373
403
|
* `dsh.bundle.patch` (one file or a LIST, #792) and the conventional root
|
|
374
404
|
* cordis.patch.yml. Ownership is judged per file, because a package may ship
|
|
375
405
|
* both and only the file's own `insert:` block says which ids it brings in.
|
package/lib/profile.js
CHANGED
|
@@ -127,12 +127,16 @@ export function readManifestDeps(profile, explicitDir) {
|
|
|
127
127
|
}
|
|
128
128
|
/**
|
|
129
129
|
* For each installed package, the OTHER installed package that declares it —
|
|
130
|
-
* as a dependency or a peer dependency — in its own
|
|
130
|
+
* as a dependency, an optional dependency, or a peer dependency — in its own
|
|
131
|
+
* manifest.
|
|
131
132
|
*
|
|
132
133
|
* pnpm's auto-install-peers writes a plugin's peers into the profile manifest
|
|
133
134
|
* as direct dependencies, so a native binding a plugin needs shows up in the
|
|
134
135
|
* installed list looking exactly like a plugin the user chose (#634). What
|
|
135
|
-
* separates them is that somebody else asked for it.
|
|
136
|
+
* separates them is that somebody else asked for it. Optional dependencies
|
|
137
|
+
* count for the same reason (#941): a platform-specific binary a plugin lists
|
|
138
|
+
* under `optionalDependencies` is payload, not a plugin the user chose, even
|
|
139
|
+
* when pnpm installed that package and the card rendered.
|
|
136
140
|
*
|
|
137
141
|
* Ownership is decided by the first owner in sorted order, so the answer does
|
|
138
142
|
* not depend on the manifest's key order. A package that declares itself is
|
|
@@ -152,7 +156,7 @@ export function readDependencyOwners(profile, names, explicitDir) {
|
|
|
152
156
|
if (typeof manifest !== 'object' || manifest === null)
|
|
153
157
|
continue;
|
|
154
158
|
const declared = manifest;
|
|
155
|
-
for (const field of [declared.dependencies, declared.peerDependencies]) {
|
|
159
|
+
for (const field of [declared.dependencies, declared.optionalDependencies, declared.peerDependencies]) {
|
|
156
160
|
if (typeof field !== 'object' || field === null)
|
|
157
161
|
continue;
|
|
158
162
|
for (const dependency of Object.keys(field)) {
|
package/lib/routes.js
CHANGED
|
@@ -45,7 +45,7 @@ import { readJsonBody, refuseUnadmitted, sameOrigin, sendJson } from './http.js'
|
|
|
45
45
|
import { resolveDshHome } from './home-paths.js';
|
|
46
46
|
import { detectedDebugger, detectedSupervisor, restartAllowed, scheduleRestart, servingPort, trustedRestartRequest, trustedDownloadRequest, trustedSelfUninstallRequest, selfUninstallReachableFrom, restartReachableFrom } from './restart.js';
|
|
47
47
|
import { activationAfterReplace, brokenClientBundles, bundlePatchIsEmpty, checkClientBundle, defaultHostRuntimeFacts, findUnservedConfigNamespaces, hasHostHalf, hostPeerGate, newlyBrokenBundles, peerGateRemedy, verifyActivation } from './verify.js';
|
|
48
|
-
import {
|
|
48
|
+
import { carrierRowIds, disableRow, enableRow, findUserPatchPath, foreignRowIds, isProtectedModule, packagePatchFlags, readUserPatchState, removeRowBlocks, rowIdsForPackage, userPatchPackageReferences, } from './patch.js';
|
|
49
49
|
import { createProfileBackup, downloadWebdav, MAX_BACKUP_BYTES, mergeRestoreManifest, restoreProfileBackup, unportableDeps, uploadWebdav, } from './backup.js';
|
|
50
50
|
import { createGist, fitsGistLimit, GistError, gistErrorCode, parseGistId, readGist, resolveGistTokenSource, updateGist, verifyGistToken, } from './gist.js';
|
|
51
51
|
import { MAX_UPDATE_OPERATIONS_V1, UpdateOperationStoreV1, UPDATE_API_V1_SCHEMA } from './update-api-v1.js';
|
|
@@ -856,9 +856,9 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup, ho
|
|
|
856
856
|
// A bundle that merely reconfigures a neighbour (config without
|
|
857
857
|
// disabled) is NOT dropped: #147 requires disabling it to leave the
|
|
858
858
|
// neighbour live, and the e2e fixture-cross re-enable breaks otherwise.
|
|
859
|
-
const
|
|
859
|
+
const carrierRows = carrierRowIds(activeProfileDir, name);
|
|
860
860
|
const foreignRows = foreignRowIds(activeProfileDir, name);
|
|
861
|
-
const isCarrier =
|
|
861
|
+
const isCarrier = carrierRows.length > 0;
|
|
862
862
|
// Both layers, or neither (#696 B). `dsh.profile.bundles` is the
|
|
863
863
|
// package-level declaration the official plugins page's switch reads
|
|
864
864
|
// and the loader composes; the patch rows above are the runtime
|
|
@@ -888,7 +888,7 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup, ho
|
|
|
888
888
|
? addProfileBundle(activeProfileDir, name)
|
|
889
889
|
: removeProfileBundle(activeProfileDir, name);
|
|
890
890
|
logEvent('info', 'toggle', isCarrier
|
|
891
|
-
? `${name}: disable-carrier ${enabled ? 're-added to' : 'removed from'} dsh.profile.bundles (
|
|
891
|
+
? `${name}: disable-carrier ${enabled ? 're-added to' : 'removed from'} dsh.profile.bundles (carrier rows: ${carrierRows.join(', ')})`
|
|
892
892
|
: reBundle
|
|
893
893
|
? `${name}: re-added to dsh.profile.bundles, which nothing was composing (#696)`
|
|
894
894
|
: `${name}: dsh.profile.bundles ${enabled ? 're-added' : 'removed'} so the official page's package switch agrees (#696)`);
|
|
@@ -989,7 +989,7 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup, ho
|
|
|
989
989
|
}
|
|
990
990
|
}
|
|
991
991
|
logEvent(ok ? 'info' : 'error', 'toggle', `${name}: ${enabled ? 'on' : 'off'} ok=${String(ok)}`);
|
|
992
|
-
return { ok, reason, patchRows, userRows, patchWrite,
|
|
992
|
+
return { ok, reason, patchRows, userRows, patchWrite, carrierRows, isCarrier, bundleSwitch };
|
|
993
993
|
}
|
|
994
994
|
/**
|
|
995
995
|
* The plugin inventory the recovery surface is allowed to switch.
|
|
@@ -1015,7 +1015,7 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup, ho
|
|
|
1015
1015
|
const plugins = [];
|
|
1016
1016
|
for (const name of names) {
|
|
1017
1017
|
const rows = rowIdsForPackage(host, activeProfileDir, name);
|
|
1018
|
-
const carrier =
|
|
1018
|
+
const carrier = carrierRowIds(activeProfileDir, name).length > 0;
|
|
1019
1019
|
if (rows.length === 0 && !carrier)
|
|
1020
1020
|
continue;
|
|
1021
1021
|
const isProtected = isProtectedModule(name);
|
|
@@ -1855,6 +1855,12 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup, ho
|
|
|
1855
1855
|
version,
|
|
1856
1856
|
requirement: verdict.requirement,
|
|
1857
1857
|
hostVersion: host?.version ?? null,
|
|
1858
|
+
// #923: the client's "find a compatible version" search is keyed by
|
|
1859
|
+
// the npm name, and the install branch could fall back to the catalog
|
|
1860
|
+
// entry's `npm` while the update branch had nothing. The value is
|
|
1861
|
+
// already here — it is this function's first argument and the key the
|
|
1862
|
+
// registry lookup above used — so publish it.
|
|
1863
|
+
npmName,
|
|
1858
1864
|
},
|
|
1859
1865
|
error: nothingWasInstalled
|
|
1860
1866
|
? `${displayName} ${version ?? ''} 要求的 DSH 版本是 ${verdict.requirement ?? '未知'},而当前运行的是 ${host?.version ?? '未知版本'},装上多半会直接报错。已停止,没有安装任何东西。 / ${displayName} ${version ?? ''} declares it needs DSH ${verdict.requirement ?? '(unknown)'}, and this host is ${host?.version ?? 'unknown'}; installing it would most likely break the plugin. Nothing was installed.`
|
|
@@ -1952,7 +1958,7 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup, ho
|
|
|
1952
1958
|
}
|
|
1953
1959
|
try {
|
|
1954
1960
|
const { channelFor, onlineSourceFor, catalogNpmByRepo } = await updateCheckInputs();
|
|
1955
|
-
const updates = await checkUpdates(config.profile, forceCheckFrom(request), activeProfileDir, channelFor, onlineSourceFor, catalogNpmByRepo);
|
|
1961
|
+
const updates = await checkUpdates(config.profile, forceCheckFrom(request), activeProfileDir, channelFor, onlineSourceFor, catalogNpmByRepo, { holdYoungReleases: config.desktopHost === true });
|
|
1956
1962
|
// `packages` carries the same objects the single-package endpoint
|
|
1957
1963
|
// returns, so one parser serves both. Only updatable ones: a badge
|
|
1958
1964
|
// wants the count, a panel wants the rows, and neither wants to
|
|
@@ -1999,7 +2005,7 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup, ho
|
|
|
1999
2005
|
// or a catalog-matched local package answers here with the
|
|
2000
2006
|
// release it can be compared against rather than with nothing.
|
|
2001
2007
|
const { channelFor, onlineSourceFor, catalogNpmByRepo } = await updateCheckInputs();
|
|
2002
|
-
const update = (await checkUpdates(config.profile, force, activeProfileDir, channelFor, onlineSourceFor, catalogNpmByRepo))[name];
|
|
2008
|
+
const update = (await checkUpdates(config.profile, force, activeProfileDir, channelFor, onlineSourceFor, catalogNpmByRepo, { holdYoungReleases: config.desktopHost === true }))[name];
|
|
2003
2009
|
if (update === undefined) {
|
|
2004
2010
|
sendJson(response, 404, { schema: UPDATE_API_V1_SCHEMA, error: 'plugin is not installed' });
|
|
2005
2011
|
return;
|
|
@@ -3060,7 +3066,7 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup, ho
|
|
|
3060
3066
|
const layers = await switchPluginLayers(name, enabled, ok, reason);
|
|
3061
3067
|
ok = layers.ok;
|
|
3062
3068
|
reason = layers.reason;
|
|
3063
|
-
const { patchRows, userRows, patchWrite,
|
|
3069
|
+
const { patchRows, userRows, patchWrite, carrierRows, isCarrier, bundleSwitch } = layers;
|
|
3064
3070
|
// Activation reads the post-write truth: the switch state OR the
|
|
3065
3071
|
// patch layer, so a disabled plugin never reports "restart to
|
|
3066
3072
|
// apply".
|
|
@@ -3104,7 +3110,7 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup, ho
|
|
|
3104
3110
|
reason,
|
|
3105
3111
|
patchRows,
|
|
3106
3112
|
patchWrite: patchWrite ?? { ok: true, reason: null },
|
|
3107
|
-
carrier:
|
|
3113
|
+
carrier: carrierRows,
|
|
3108
3114
|
bundleSwitch,
|
|
3109
3115
|
restart,
|
|
3110
3116
|
refresh,
|
|
@@ -3670,7 +3676,7 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup, ho
|
|
|
3670
3676
|
catch (error) {
|
|
3671
3677
|
logEvent('warn', 'updates', `package source lookup failed — ${error instanceof Error ? error.message : String(error)}`);
|
|
3672
3678
|
}
|
|
3673
|
-
const updates = await checkUpdates(config.profile, force, activeProfileDir, channelFor, onlineSourceFor, catalogNpmByRepo);
|
|
3679
|
+
const updates = await checkUpdates(config.profile, force, activeProfileDir, channelFor, onlineSourceFor, catalogNpmByRepo, { holdYoungReleases: config.desktopHost === true });
|
|
3674
3680
|
for (const [name, migration] of sourceMigrationFor) {
|
|
3675
3681
|
const status = updates[name];
|
|
3676
3682
|
if (status !== undefined)
|
|
@@ -4478,6 +4484,12 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup, ho
|
|
|
4478
4484
|
if (pinnedForIdentification) {
|
|
4479
4485
|
declareProfileDependency(config.profile, name, pinValue, activeProfileDir);
|
|
4480
4486
|
}
|
|
4487
|
+
// #928: an update is remove-then-add, and a host killed between
|
|
4488
|
+
// the two leaves a package that is simply gone — with no record
|
|
4489
|
+
// of what was being attempted, because the completion log never
|
|
4490
|
+
// runs. Write the intent first, so the log can answer "where did
|
|
4491
|
+
// my plugin go" after the fact.
|
|
4492
|
+
logEvent('info', 'update-start', `${name} -> ${target} (profile ${config.profile})`);
|
|
4481
4493
|
const result = await runPlugin(config.profile, addArgs);
|
|
4482
4494
|
const cancelled = result.cancelled;
|
|
4483
4495
|
// The pin above is the market's own write, so it is undone whenever
|
|
@@ -5745,6 +5757,10 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup, ho
|
|
|
5745
5757
|
// it is meant to clean up after, and the rows outlive the package
|
|
5746
5758
|
// as boot-time orphans (#799).
|
|
5747
5759
|
const ownedRows = rowIdsForPackage(host, activeProfileDir, name);
|
|
5760
|
+
// #928: the same intent record as the update path — an uninstall
|
|
5761
|
+
// interrupted mid-remove leaves the same "where did it go" with no
|
|
5762
|
+
// entry saying what was running.
|
|
5763
|
+
logEvent('info', 'uninstall-start', `${name} (profile ${config.profile})`);
|
|
5748
5764
|
const result = await runPlugin(config.profile, ['remove', name]);
|
|
5749
5765
|
const cancelled = result.cancelled;
|
|
5750
5766
|
const ok = result.exitCode === 0 && !result.timedOut && !cancelled;
|
package/lib/trial.js
CHANGED
|
@@ -93,7 +93,7 @@ export function trialValidate(profileDir, newCommunityOrder, options = {}) {
|
|
|
93
93
|
// id into one entry (1.0.4+) this is an override, not a refusal. Making it
|
|
94
94
|
// fatal here is what rolled back every update for a profile that already
|
|
95
95
|
// had a package row and a user-patch row on one id.
|
|
96
|
-
const duplicateFatal = duplicateEntryIdsAreFatal(profileDir);
|
|
96
|
+
const duplicateFatal = duplicateEntryIdsAreFatal(profileDir, dshInstall);
|
|
97
97
|
for (const dup of composed.duplicates) {
|
|
98
98
|
const message = `duplicate loader entry id ${JSON.stringify(dup.id)} (${dup.count} rows) / 重复的 loader 条目 id ${JSON.stringify(dup.id)}`;
|
|
99
99
|
if (duplicateFatal)
|
package/lib/types/check.d.ts
CHANGED
|
@@ -302,10 +302,15 @@ export declare function composeLayers(layers: LayerInput[]): Composed;
|
|
|
302
302
|
* patch row plus a user-patch row on the same id: fatal forever meant every
|
|
303
303
|
* update of every plugin rolled back.
|
|
304
304
|
*
|
|
305
|
-
*
|
|
306
|
-
* the
|
|
305
|
+
* An empty answer from the PROFILE lockfile is not "unknown" on the official
|
|
306
|
+
* Desktop: that host embeds the loader in `app.asar` and provides it to every
|
|
307
|
+
* profile, so no profile can ever name it (#943). Reading that as an old
|
|
308
|
+
* loader made the gate fatal for every update on 0.2.1-rc.2 — the exact report
|
|
309
|
+
* #853's fix was supposed to close. The host installation is asked before the
|
|
310
|
+
* evidence is treated as missing; only when both are silent does the
|
|
311
|
+
* conservative answer stand.
|
|
307
312
|
*/
|
|
308
|
-
export declare function duplicateEntryIdsAreFatal(profileDirectory: string): boolean;
|
|
313
|
+
export declare function duplicateEntryIdsAreFatal(profileDirectory: string, dshInstallDir?: string | null): boolean;
|
|
309
314
|
/**
|
|
310
315
|
* Build the bundle layer stack for a profile under a GIVEN bundle order —
|
|
311
316
|
* the manifest order for analyzeProfile, or a candidate order for trial
|
package/lib/types/patch.d.ts
CHANGED
|
@@ -85,27 +85,32 @@ export declare function userPatchPackageReferences(patchPath: string, packageNam
|
|
|
85
85
|
*/
|
|
86
86
|
export declare function rowIdsForPackage(host: PatchHost, profileDirectory: string, packageName: string): string[];
|
|
87
87
|
/**
|
|
88
|
-
* The ids of OTHER plugins a package's bundle patch
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
88
|
+
* The ids of OTHER plugins a package's bundle patch rewires — top-level rows
|
|
89
|
+
* that either `disabled: true` a plugin it does not own, or add an `inject:`
|
|
90
|
+
* requirement to one.
|
|
91
|
+
*
|
|
92
|
+
* Both are the marker of a bundle whose toggle-off can brick the boot while
|
|
93
|
+
* its patch keeps applying (#224, #938):
|
|
94
|
+
* - dsh-postgres-backends disables session-persistence-jsonl, so once the
|
|
95
|
+
* market also disables the postgres backends nothing provides
|
|
96
|
+
* sessionPersistence;
|
|
97
|
+
* - billion-context adds `inject: [biliBasicCompactionPolicy]` to the four
|
|
98
|
+
* official agent presets, so disabling only its own row leaves the presets
|
|
99
|
+
* waiting for a service that no longer exists — every preset disappears.
|
|
93
100
|
*
|
|
94
101
|
* A bundle that merely RECONFIGURES a neighbour is deliberately NOT counted:
|
|
95
102
|
* the e2e fixture-cross tweaks dshm-fixture-b's config, and #147 requires
|
|
96
103
|
* disabling it to leave that neighbour live — dropping such a bundle from the
|
|
97
104
|
* stack broke its re-enable. Config-only side effects stay on the normal #147
|
|
98
|
-
* path
|
|
99
|
-
*
|
|
100
|
-
* whole patch stops applying.
|
|
105
|
+
* path. Removing the bundle still neutralizes any config side effects it
|
|
106
|
+
* carries, since its whole patch stops applying.
|
|
101
107
|
*
|
|
102
108
|
* Reads both patch sources like rowIdsForPackage — the declared
|
|
103
109
|
* dsh.bundle.patch (one file or a LIST of them, #792) and the conventional
|
|
104
110
|
* root cordis.patch.yml — so either form is detected, and every declared file
|
|
105
|
-
* is attributed
|
|
106
|
-
* bricks the boot, #224).
|
|
111
|
+
* is attributed.
|
|
107
112
|
*/
|
|
108
|
-
export declare function
|
|
113
|
+
export declare function carrierRowIds(profileDirectory: string, packageName: string): string[];
|
|
109
114
|
/**
|
|
110
115
|
* The ids a package's bundle patch touches WITHOUT inserting them itself.
|
|
111
116
|
*
|
|
@@ -121,7 +126,7 @@ export declare function carrierDisableIds(profileDirectory: string, packageName:
|
|
|
121
126
|
* fixture-cross e2e exist to protect, where the neighbour must keep running
|
|
122
127
|
* and the bundle must keep re-enabling.
|
|
123
128
|
*
|
|
124
|
-
* Reads both patch sources like
|
|
129
|
+
* Reads both patch sources like carrierRowIds: the declared
|
|
125
130
|
* `dsh.bundle.patch` (one file or a LIST, #792) and the conventional root
|
|
126
131
|
* cordis.patch.yml. Ownership is judged per file, because a package may ship
|
|
127
132
|
* both and only the file's own `insert:` block says which ids it brings in.
|
package/lib/types/profile.d.ts
CHANGED
|
@@ -55,12 +55,16 @@ export declare function generationManagedNames(profile: string, explicitDir?: st
|
|
|
55
55
|
export declare function readManifestDeps(profile: string, explicitDir?: string): Record<string, string>;
|
|
56
56
|
/**
|
|
57
57
|
* For each installed package, the OTHER installed package that declares it —
|
|
58
|
-
* as a dependency or a peer dependency — in its own
|
|
58
|
+
* as a dependency, an optional dependency, or a peer dependency — in its own
|
|
59
|
+
* manifest.
|
|
59
60
|
*
|
|
60
61
|
* pnpm's auto-install-peers writes a plugin's peers into the profile manifest
|
|
61
62
|
* as direct dependencies, so a native binding a plugin needs shows up in the
|
|
62
63
|
* installed list looking exactly like a plugin the user chose (#634). What
|
|
63
|
-
* separates them is that somebody else asked for it.
|
|
64
|
+
* separates them is that somebody else asked for it. Optional dependencies
|
|
65
|
+
* count for the same reason (#941): a platform-specific binary a plugin lists
|
|
66
|
+
* under `optionalDependencies` is payload, not a plugin the user chose, even
|
|
67
|
+
* when pnpm installed that package and the card rendered.
|
|
64
68
|
*
|
|
65
69
|
* Ownership is decided by the first owner in sorted order, so the answer does
|
|
66
70
|
* not depend on the manifest's key order. A package that declares itself is
|
package/lib/types/updates.d.ts
CHANGED
|
@@ -63,6 +63,18 @@ export interface UpdateStatus {
|
|
|
63
63
|
repo: string;
|
|
64
64
|
target: string;
|
|
65
65
|
};
|
|
66
|
+
/**
|
|
67
|
+
* A newer release exists but this profile's `minimumReleaseAge` will not let
|
|
68
|
+
* pnpm install it yet (#945). The official Desktop takes no one-shot bypass,
|
|
69
|
+
* so offering the update there means offering a button that is guaranteed
|
|
70
|
+
* to fail; the market names the wait instead. `updateAvailable` is false —
|
|
71
|
+
* an update the user cannot apply is not available — and `until` is when
|
|
72
|
+
* pnpm will accept it.
|
|
73
|
+
*/
|
|
74
|
+
heldUpdate?: {
|
|
75
|
+
latest: string;
|
|
76
|
+
until: string;
|
|
77
|
+
};
|
|
66
78
|
/**
|
|
67
79
|
* The version this package's channel points at, when it differs from what
|
|
68
80
|
* is installed and is NOT newer.
|
|
@@ -210,4 +222,14 @@ onlineSourceFor?: ReadonlyMap<string, string>,
|
|
|
210
222
|
* entry that owns both the repo and the name, the npm fallback would
|
|
211
223
|
* compare the install with whatever package merely shares its name.
|
|
212
224
|
*/
|
|
213
|
-
catalogNpmByRepo?: ReadonlyMap<string, string
|
|
225
|
+
catalogNpmByRepo?: ReadonlyMap<string, string>,
|
|
226
|
+
/**
|
|
227
|
+
* Hold back releases that this profile's `minimumReleaseAge` will refuse
|
|
228
|
+
* (#945). Only true where the install path has no bypass: the official
|
|
229
|
+
* Desktop's manager takes no pnpm options, while the CLI path retries once
|
|
230
|
+
* with `--config.minimum-release-age=0` and CAN install a young release.
|
|
231
|
+
* Hiding those on the CLI would suppress updates that work.
|
|
232
|
+
*/
|
|
233
|
+
options?: {
|
|
234
|
+
holdYoungReleases?: boolean;
|
|
235
|
+
}): Promise<Record<string, UpdateStatus>>;
|