dshmarket 1.66.22 → 1.66.24
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 +48 -14
- package/lib/changelog.js +9 -1
- package/lib/install.js +31 -5
- package/lib/types/changelog.d.ts +3 -2
- package/lib/verify.js +93 -4
- package/package.json +2 -2
- package/src/changelog.ts +12 -3
- package/src/client/MarketSection.tsx +57 -11
- package/src/client/locales.ts +6 -0
- package/src/install.ts +29 -5
- package/src/verify.ts +92 -4
package/client/client.js
CHANGED
|
@@ -395,6 +395,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
395
395
|
notesNpm: "版本发布时间",
|
|
396
396
|
notesNone: "该插件未提供更新说明。",
|
|
397
397
|
notesSource: "市场还没收录这个插件的更新说明。",
|
|
398
|
+
notesSourceUnreadable: "这个插件不是从 GitHub 装的,市场读不到它的改动记录。",
|
|
398
399
|
notesSourceLink: "去仓库看改了什么",
|
|
399
400
|
notesSourceNoLink: "可以问问插件作者改了什么。",
|
|
400
401
|
notesLoadFail: "暂时读不到更新说明,稍后再试。",
|
|
@@ -557,6 +558,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
557
558
|
marketUpdate: "更新插件市场",
|
|
558
559
|
updateAll: "全部更新",
|
|
559
560
|
heldReleaseNotice: "已安装,但不是最新版:这个 profile 的 minimumReleaseAge 挡住了 {0},安装的是 {1}。",
|
|
561
|
+
installedDependencyNote: "另有 {0} 个包是插件带进来的依赖,不是插件,未列在上方。",
|
|
560
562
|
heldReleaseAction: "仍安装 {0}",
|
|
561
563
|
ignoreUpdateNotice: "重启前不再提醒",
|
|
562
564
|
ignoreAllUpdateNotices: "重启前都不再提醒",
|
|
@@ -588,6 +590,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
588
590
|
webdavRestore: "从 WebDAV 恢复",
|
|
589
591
|
autoBackup: "每天自动备份(打开市场时)",
|
|
590
592
|
webdavNote: "WebDAV 地址与用户名仅保存在当前浏览器;密码只存于服务端,每次会话需重新输入。",
|
|
593
|
+
webdavPathHint: "填完整的文件地址(含文件名):备份写进这个文件,恢复也从它读。开启自动备份后每天最多上传一次。",
|
|
591
594
|
localOnly: "WebDAV 地址和用户名只保存在此浏览器。",
|
|
592
595
|
credsWarning: "请注意:备份包含 profile 配置,以及可能含密钥的文件(config.toml、.env 等)。本地导出的文件会原样包含这些内容,上传 WebDAV 前请确认目标可信。",
|
|
593
596
|
gist: "GitHub Gist",
|
|
@@ -1111,6 +1114,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
1111
1114
|
notesNpm: "Version publish times",
|
|
1112
1115
|
notesNone: "This plugin does not publish update notes.",
|
|
1113
1116
|
notesSource: "The market doesn't have update notes for this plugin yet.",
|
|
1117
|
+
notesSourceUnreadable: "This plugin was not installed from GitHub, so the market cannot read its change history.",
|
|
1114
1118
|
notesSourceLink: "See what changed in the repository",
|
|
1115
1119
|
notesSourceNoLink: "You can ask the plugin author what changed.",
|
|
1116
1120
|
notesLoadFail: "Update notes are unavailable right now — try again later.",
|
|
@@ -1273,6 +1277,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
1273
1277
|
marketUpdate: "Update the plugin market",
|
|
1274
1278
|
updateAll: "Update all",
|
|
1275
1279
|
heldReleaseNotice: "Installed, but not the newest: this profile's minimumReleaseAge held {0} back, so {1} was installed.",
|
|
1280
|
+
installedDependencyNote: "{0} more packages came in as dependencies of plugins — they are not plugins, so they are not listed above.",
|
|
1276
1281
|
heldReleaseAction: "Install {0} anyway",
|
|
1277
1282
|
ignoreUpdateNotice: "Don't remind until restart",
|
|
1278
1283
|
ignoreAllUpdateNotices: "Don't remind any until restart",
|
|
@@ -1304,6 +1309,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
1304
1309
|
webdavRestore: "Restore from WebDAV",
|
|
1305
1310
|
autoBackup: "Back up daily (when the market opens)",
|
|
1306
1311
|
webdavNote: "The WebDAV URL and username stay in this browser only; the password is kept server-side and must be re-entered each session.",
|
|
1312
|
+
webdavPathHint: "Give the whole file path, name included: the backup writes that file and restore reads it back. With automatic backup on it uploads at most once a day.",
|
|
1307
1313
|
localOnly: "The WebDAV URL and username stay in this browser only.",
|
|
1308
1314
|
credsWarning: "Note: backups include profile configuration and files that may hold secrets (config.toml, .env, …). Local exports are unmodified, so upload only to a WebDAV target you trust.",
|
|
1309
1315
|
gist: "GitHub Gist",
|
|
@@ -8589,6 +8595,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
8589
8595
|
const [restarting, setRestarting] = (0, react.useState)(false);
|
|
8590
8596
|
const [showTop, setShowTop] = (0, react.useState)(false);
|
|
8591
8597
|
const [backupBusy, setBackupBusy] = (0, react.useState)(false);
|
|
8598
|
+
const webdavInFlight = (0, react.useRef)(false);
|
|
8592
8599
|
const [backupMessage, setBackupMessage] = (0, react.useState)(null);
|
|
8593
8600
|
const [backupRestored, setBackupRestored] = (0, react.useState)(false);
|
|
8594
8601
|
const [pendingBackup, setPendingBackup] = (0, react.useState)(null);
|
|
@@ -10641,7 +10648,19 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
10641
10648
|
const reminderUpdatableNames = updatableNames.filter((name) => !quietUpdateSet.has(name) && !installedBlocked(name));
|
|
10642
10649
|
const reminderBatchUpdatableNames = batchUpdatableNames.filter((name) => !quietUpdateSet.has(name) && !installedBlocked(name));
|
|
10643
10650
|
const reminderUpdateNames = [...updates[selfName]?.updateAvailable === true && !updatedNames.includes(selfName) ? [selfName] : [], ...updatableNames].filter((name) => !quietUpdateSet.has(name) && !installedBlocked(name));
|
|
10644
|
-
|
|
10651
|
+
/**
|
|
10652
|
+
* Installed packages that are not plugins at all: a plain library some
|
|
10653
|
+
* other plugin pulled in — zustand, bufferutil, a peer of ws (#634's
|
|
10654
|
+
* `dependencyOf`, #857). They were rendered as plugin cards with a
|
|
10655
|
+
* working uninstall, which is a real remove path for something a plugin
|
|
10656
|
+
* needs. Out of the list; the count stays visible in one quiet line, so the
|
|
10657
|
+
* list is not silently shorter than package.json.
|
|
10658
|
+
*/
|
|
10659
|
+
const dependencyOnlyNames = (0, react.useMemo)(() => new Set(Object.keys(installed).filter((name) => {
|
|
10660
|
+
const act = activations[name];
|
|
10661
|
+
return act?.state === "inert" && act.dependencyOf !== void 0;
|
|
10662
|
+
})), [installed, activations]);
|
|
10663
|
+
const installedOtherCount = Object.keys(installed).filter((name) => name !== selfName && !dependencyOnlyNames.has(name)).length;
|
|
10645
10664
|
const ignoreUpdateNotices = (0, react.useCallback)((names) => {
|
|
10646
10665
|
if (bootId === null || names.length === 0) return;
|
|
10647
10666
|
setIgnoredUpdateNames((current) => {
|
|
@@ -10729,7 +10748,8 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
10729
10748
|
}).catch((error) => setBackupMessage(String(error))).finally(() => setBackupBusy(false));
|
|
10730
10749
|
}, [finishRestore, pendingBackup]);
|
|
10731
10750
|
const runWebdav = (0, react.useCallback)((action) => {
|
|
10732
|
-
if (webdavUrl.trim() === "") return;
|
|
10751
|
+
if (webdavUrl.trim() === "" || webdavInFlight.current) return;
|
|
10752
|
+
webdavInFlight.current = true;
|
|
10733
10753
|
setBackupBusy(true);
|
|
10734
10754
|
setBackupMessage(null);
|
|
10735
10755
|
setRestoreErrors([]);
|
|
@@ -10752,7 +10772,10 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
10752
10772
|
} catch {}
|
|
10753
10773
|
setBackupMessage(t("backupDone"));
|
|
10754
10774
|
}
|
|
10755
|
-
}).catch((error) => setBackupMessage(String(error))).finally(() =>
|
|
10775
|
+
}).catch((error) => setBackupMessage(String(error))).finally(() => {
|
|
10776
|
+
webdavInFlight.current = false;
|
|
10777
|
+
setBackupBusy(false);
|
|
10778
|
+
});
|
|
10756
10779
|
}, [
|
|
10757
10780
|
previewBackup,
|
|
10758
10781
|
t,
|
|
@@ -10876,6 +10899,10 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
10876
10899
|
setExportIncludeConfig(false);
|
|
10877
10900
|
setExportOpen(true);
|
|
10878
10901
|
}, [installed, installedBundles]);
|
|
10902
|
+
const runWebdavRef = (0, react.useRef)(runWebdav);
|
|
10903
|
+
(0, react.useEffect)(() => {
|
|
10904
|
+
runWebdavRef.current = runWebdav;
|
|
10905
|
+
}, [runWebdav]);
|
|
10879
10906
|
(0, react.useEffect)(() => {
|
|
10880
10907
|
try {
|
|
10881
10908
|
localStorage.setItem(WEBDAV_STORAGE_KEY, JSON.stringify({
|
|
@@ -10884,18 +10911,19 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
10884
10911
|
auto: autoBackup
|
|
10885
10912
|
}));
|
|
10886
10913
|
} catch {}
|
|
10887
|
-
if (!autoBackup || webdavUrl.trim() === "") return;
|
|
10888
|
-
let last = 0;
|
|
10889
|
-
try {
|
|
10890
|
-
last = Number(localStorage.getItem("dshm-webdav-last")) || 0;
|
|
10891
|
-
} catch {}
|
|
10892
|
-
if (Date.now() - last >= 864e5) runWebdav("backup");
|
|
10893
10914
|
}, [
|
|
10894
10915
|
autoBackup,
|
|
10895
|
-
runWebdav,
|
|
10896
10916
|
webdavUrl,
|
|
10897
10917
|
webdavUser
|
|
10898
10918
|
]);
|
|
10919
|
+
(0, react.useEffect)(() => {
|
|
10920
|
+
if (!autoBackup) return;
|
|
10921
|
+
let last = 0;
|
|
10922
|
+
try {
|
|
10923
|
+
last = Number(localStorage.getItem("dshm-webdav-last")) || 0;
|
|
10924
|
+
} catch {}
|
|
10925
|
+
if (Date.now() - last >= 864e5) runWebdavRef.current("backup");
|
|
10926
|
+
}, [autoBackup]);
|
|
10899
10927
|
const sessionPendingRestart = doneUrls.length + restartNames.length + removedCount + toggleRestart + (backupRestored ? 1 : 0);
|
|
10900
10928
|
/**
|
|
10901
10929
|
* Plugins the HOST reports as restart-pending, independent of what this
|
|
@@ -10936,7 +10964,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
10936
10964
|
const isInstalledListActive = tab === "installed" && installedView === "list";
|
|
10937
10965
|
const updatesLoaded = Object.keys(updates).length > 0;
|
|
10938
10966
|
const orderedInstalledEntries = (0, react.useMemo)(() => {
|
|
10939
|
-
return Object.entries(displayedInstalled).filter(([name]) => name !== selfName).sort(([nameA, specA], [nameB, specB]) => {
|
|
10967
|
+
return Object.entries(displayedInstalled).filter(([name]) => name !== selfName).filter(([name]) => !dependencyOnlyNames.has(name)).sort(([nameA, specA], [nameB, specB]) => {
|
|
10940
10968
|
const aOn = effectiveDisabledSet.has(nameA) ? 0 : 1;
|
|
10941
10969
|
const bOn = effectiveDisabledSet.has(nameB) ? 0 : 1;
|
|
10942
10970
|
if (aOn !== bOn) return bOn - aOn;
|
|
@@ -10948,7 +10976,8 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
10948
10976
|
displayedInstalled,
|
|
10949
10977
|
selfName,
|
|
10950
10978
|
updatesLoaded,
|
|
10951
|
-
effectiveDisabledSet
|
|
10979
|
+
effectiveDisabledSet,
|
|
10980
|
+
dependencyOnlyNames
|
|
10952
10981
|
]);
|
|
10953
10982
|
const missingRestoreCount = Object.keys(pendingDependencies).filter((name) => !installedFiles.includes(name)).length;
|
|
10954
10983
|
const hasUpdates = reminderUpdatableNames.length > 0;
|
|
@@ -11770,7 +11799,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
11770
11799
|
*/
|
|
11771
11800
|
const pendingRefreshNames = (0, react.useMemo)(() => [.../* @__PURE__ */ new Set([...hotNames, ...refreshNames])], [hotNames, refreshNames]);
|
|
11772
11801
|
/** Installed plugins the market itself cannot group (#60). */
|
|
11773
|
-
const groupableNames = Object.keys(installed).filter((name) => name !== "dsh-market" && name !== "dshmarket");
|
|
11802
|
+
const groupableNames = Object.keys(installed).filter((name) => name !== "dsh-market" && name !== "dshmarket" && !dependencyOnlyNames.has(name));
|
|
11774
11803
|
/** Names already inside some group; everything else shows under "ungrouped". */
|
|
11775
11804
|
const groupedNames = (0, react.useMemo)(() => new Set(Object.values(groups).flat()), [groups]);
|
|
11776
11805
|
const ungroupedNames = groupableNames.filter((name) => !groupedNames.has(name));
|
|
@@ -12445,6 +12474,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
12445
12474
|
placeholder: t("webdavUrl"),
|
|
12446
12475
|
onChange: (e) => setWebdavUrl(e.target.value)
|
|
12447
12476
|
}),
|
|
12477
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("webdavPathHint") }),
|
|
12448
12478
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
|
|
12449
12479
|
className: Market_module_css_default.backupInput,
|
|
12450
12480
|
autoComplete: "username",
|
|
@@ -13641,6 +13671,10 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
13641
13671
|
})]
|
|
13642
13672
|
}, name);
|
|
13643
13673
|
}
|
|
13674
|
+
}),
|
|
13675
|
+
dependencyOnlyNames.size > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
13676
|
+
className: Market_module_css_default.groupOrgHint,
|
|
13677
|
+
children: t("installedDependencyNote").replace("{0}", String(dependencyOnlyNames.size))
|
|
13644
13678
|
})
|
|
13645
13679
|
] })
|
|
13646
13680
|
}),
|
|
@@ -14264,7 +14298,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
14264
14298
|
})]
|
|
14265
14299
|
}) : updateNotes.kind === "source" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
14266
14300
|
className: Market_module_css_default.spec,
|
|
14267
|
-
children: [t(updateNotes.reason === "unavailable" ? "notesLoadFail" : "notesSource"), typeof updateNotes.sourceUrl === "string" && /^https?:\/\//i.test(updateNotes.sourceUrl) ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [lang === "zh" ? "" : " ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
14301
|
+
children: [t(updateNotes.reason === "unavailable" ? "notesLoadFail" : updateNotes.reason === "unreadable" ? "notesSourceUnreadable" : "notesSource"), typeof updateNotes.sourceUrl === "string" && /^https?:\/\//i.test(updateNotes.sourceUrl) ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [lang === "zh" ? "" : " ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
14268
14302
|
className: Market_module_css_default.notesSourceLink,
|
|
14269
14303
|
href: updateNotes.sourceUrl,
|
|
14270
14304
|
target: "_blank",
|
package/lib/changelog.js
CHANGED
|
@@ -341,9 +341,17 @@ export async function updateNotesFor(profile, explicitDir, name) {
|
|
|
341
341
|
const specKey = repoKeyOf(spec) ?? lookupRepoFromUrl(spec);
|
|
342
342
|
// The market holds no notes for this install, but its history is still
|
|
343
343
|
// readable where it came from: say which, and point there.
|
|
344
|
+
//
|
|
345
|
+
// Notes come from the catalog's own release feed, which is built from
|
|
346
|
+
// GitHub, so a gitee or self-hosted git install has no feed to find:
|
|
347
|
+
// "the market has not collected this one yet" would promise something that
|
|
348
|
+
// cannot happen, and the reader waits for it (#819). Those say the source
|
|
349
|
+
// cannot be read instead. The url is the same either way — the repository
|
|
350
|
+
// is where the answer actually is.
|
|
351
|
+
const unreadableSource = githubIdentityOf(spec) === null && isGitHostedSpec(spec);
|
|
344
352
|
const fromSource = (reason) => ({
|
|
345
353
|
kind: 'source',
|
|
346
|
-
reason,
|
|
354
|
+
reason: unreadableSource ? 'unreadable' : reason,
|
|
347
355
|
sourceUrl: sourceWebUrl(spec) ?? specKey,
|
|
348
356
|
});
|
|
349
357
|
try {
|
package/lib/install.js
CHANGED
|
@@ -328,10 +328,16 @@ export async function withHoistRecovery(run, profile, pluginArgs, profileDirecto
|
|
|
328
328
|
// no options from us — but the classified message above says the market
|
|
329
329
|
// retried. Say that it did not, or the user reads a retry that never
|
|
330
330
|
// happened and looks for the wrong cause.
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
331
|
+
//
|
|
332
|
+
// The wait on a fresh release is the one of these where "no retry" leaves
|
|
333
|
+
// the user with nothing to do at all: the plugin IS published, pnpm just
|
|
334
|
+
// will not take it yet, and the door the market would open on a plain
|
|
335
|
+
// `dsh web` host is shut here — so say when the same button starts
|
|
336
|
+
// working, and what to use meanwhile (#852).
|
|
337
|
+
const note = unavailableOption === RELEASE_AGE_OVERRIDE
|
|
338
|
+
? `这个插件的新版本比本 profile 的 minimumReleaseAge(新版本等待期)还新,pnpm 因此拒绝了本次改动(改别的插件也一样)。官方桌面端由应用自己执行安装,不接受市场的临时放行参数(${unavailableOption}),所以这里没有自动重试。**等这个版本过了等待期再更新一次即可**;想现在就更新,可以在普通的 dsh web 里装。 / The plugin's new version is younger than this profile's minimumReleaseAge, so pnpm refused this change (a change to another plugin too). The official desktop app runs the install itself and takes no one-shot bypass from the market (${unavailableOption}), so no automatic retry happens here. **Update again once the version is past the wait**; to update right now, install from plain dsh web.`
|
|
339
|
+
: `这台宿主(官方桌面端)自己执行安装,不接受市场附加的 pnpm 参数(${unavailableOption}),所以这一步没有自动重试。 / This host (the official desktop app) runs the install itself and takes no pnpm options from the market (${unavailableOption}), so that automatic retry could not be attempted.`;
|
|
340
|
+
result = { ...result, stderr: `${result.stderr}\n\n${note}` };
|
|
335
341
|
}
|
|
336
342
|
return result;
|
|
337
343
|
}
|
|
@@ -426,7 +432,27 @@ export function failureDetail(result, limit = 300) {
|
|
|
426
432
|
const code = result.pnpmErrorCode === undefined ? '' : `${result.pnpmErrorCode}: `;
|
|
427
433
|
return `${code}${result.pnpmError}`.slice(0, limit);
|
|
428
434
|
}
|
|
429
|
-
|
|
435
|
+
const text = (result.stderr || result.stdout).trim();
|
|
436
|
+
if (text.length <= limit)
|
|
437
|
+
return text;
|
|
438
|
+
const tail = text.slice(-limit);
|
|
439
|
+
// Cut at a sentence end instead of mid-word, and say that something was
|
|
440
|
+
// dropped (#852). The market appends its own explanation to pnpm's output,
|
|
441
|
+
// and this tail is catalogued into user-facing sentences — including inside
|
|
442
|
+
// a parenthesis. A blunt cut landed inside the previous sentence and welded
|
|
443
|
+
// the fragment to the next one: the reporter's log read
|
|
444
|
+
// `(is, export the log and report it这台宿主…`, which is not a sentence in
|
|
445
|
+
// either language and says nothing about what to do.
|
|
446
|
+
const sentenceEnd = /[.!?。!?]["')\])]?[\s\n]+/gu;
|
|
447
|
+
const match = sentenceEnd.exec(tail);
|
|
448
|
+
if (match !== null) {
|
|
449
|
+
const rest = tail.slice(match.index + match[0].length).trim();
|
|
450
|
+
// A boundary so near the end would leave a word or two; the raw tail is
|
|
451
|
+
// more use than the stub.
|
|
452
|
+
if (rest.length >= 40)
|
|
453
|
+
return `… ${rest}`;
|
|
454
|
+
}
|
|
455
|
+
return `…${tail.trim()}`;
|
|
430
456
|
}
|
|
431
457
|
/**
|
|
432
458
|
* Some registry entries point at collection repos whose actual plugin lives
|
package/lib/types/changelog.d.ts
CHANGED
|
@@ -106,9 +106,10 @@ export interface UpdateNotes {
|
|
|
106
106
|
/**
|
|
107
107
|
* Present for `source`: `missing` when the notes loaded without this
|
|
108
108
|
* plugin, `unavailable` when they could not be loaded at all — a passing
|
|
109
|
-
* failure, not a fact about the plugin
|
|
109
|
+
* failure, not a fact about the plugin — and `unreadable` when the source is
|
|
110
|
+
* one the market can never read, so waiting will not help (#819).
|
|
110
111
|
*/
|
|
111
|
-
reason?: 'missing' | 'unavailable';
|
|
112
|
+
reason?: 'missing' | 'unavailable' | 'unreadable';
|
|
112
113
|
}
|
|
113
114
|
/**
|
|
114
115
|
* Resolve the notes for one installed plugin, or `{ kind: 'none' }`.
|
package/lib/verify.js
CHANGED
|
@@ -26,14 +26,15 @@
|
|
|
26
26
|
* or a declared entry artifact that is missing
|
|
27
27
|
* missing – not present in node_modules
|
|
28
28
|
*/
|
|
29
|
-
import { readFileSync } from 'node:fs';
|
|
29
|
+
import { readFileSync, realpathSync, statSync } from 'node:fs';
|
|
30
|
+
import { createRequire, isBuiltin } from 'node:module';
|
|
30
31
|
import { Script } from 'node:vm';
|
|
31
32
|
import { join } from 'node:path';
|
|
32
33
|
import { listHotMounts, parseSimplePatch } from './hot.js';
|
|
33
34
|
import { userPatchPackageReferences } from './patch.js';
|
|
34
35
|
import { nameMatchesPackage } from './entry-identity.js';
|
|
35
36
|
import { bundlePatchInsertedIds, declaredBundlePatchFiles, hasDshManifest, hasLoadableEntry, profileDir, readInstalled } from './profile.js';
|
|
36
|
-
import { compareSemver, parsePatchFile, satisfiesRange } from './check.js';
|
|
37
|
+
import { compareSemver, composeLayers, parsePatchFile, satisfiesRange } from './check.js';
|
|
37
38
|
import { dshHostInfo } from './dsh-install.js';
|
|
38
39
|
/**
|
|
39
40
|
* Whether every bundle patch the package declares parses to an empty list.
|
|
@@ -183,11 +184,15 @@ const isDshPeer = (peer) => peer === '@deepseek-ai/dsh' || peer.startsWith('@dee
|
|
|
183
184
|
* peers fail at once.
|
|
184
185
|
*/
|
|
185
186
|
export function hostPeerGate(activeProfileDir, name, facts) {
|
|
187
|
+
return peerGateAt(join(activeProfileDir, 'node_modules', name), name, facts);
|
|
188
|
+
}
|
|
189
|
+
/** Same prediction for a patch plugin resolved outside the profile's flat node_modules. */
|
|
190
|
+
function peerGateAt(packageDir, name, facts) {
|
|
186
191
|
if (facts.runtimeVersion === null)
|
|
187
192
|
return null;
|
|
188
193
|
let manifest;
|
|
189
194
|
try {
|
|
190
|
-
manifest = JSON.parse(readFileSync(join(
|
|
195
|
+
manifest = JSON.parse(readFileSync(join(packageDir, 'package.json'), 'utf8'));
|
|
191
196
|
}
|
|
192
197
|
catch {
|
|
193
198
|
return null;
|
|
@@ -215,6 +220,74 @@ export function hostPeerGate(activeProfileDir, name, facts) {
|
|
|
215
220
|
}
|
|
216
221
|
return null;
|
|
217
222
|
}
|
|
223
|
+
/**
|
|
224
|
+
* #867: only the bundle's own effective loader rows are evidence, never its
|
|
225
|
+
* ordinary dependency closure or arbitrary names in plugin configuration.
|
|
226
|
+
* Compose every declared file together so later disables take effect. An
|
|
227
|
+
* unreadable file or an expression-gated row gives no rejection evidence.
|
|
228
|
+
*/
|
|
229
|
+
function bundleRowPeerGate(packageDir, activeProfileDir, facts) {
|
|
230
|
+
if (facts.runtimeVersion === null)
|
|
231
|
+
return null;
|
|
232
|
+
try {
|
|
233
|
+
const patches = declaredBundlePatchFiles(packageDir).map(parsePatchFile);
|
|
234
|
+
if (patches.some(rows => rows === null))
|
|
235
|
+
return null;
|
|
236
|
+
const { resolvableRows } = composeLayers([{
|
|
237
|
+
label: 'bundle', kind: 'bundle', patches: patches.flatMap(rows => rows ?? []), parseError: null,
|
|
238
|
+
}]);
|
|
239
|
+
const checked = new Set();
|
|
240
|
+
for (const row of resolvableRows) {
|
|
241
|
+
if (row.activation !== 'required' || typeof row.name !== 'string')
|
|
242
|
+
continue;
|
|
243
|
+
// Bare npm names and subpath entries only. Relative files, builtins,
|
|
244
|
+
// aliases and expressions need runtime resolution; do not guess.
|
|
245
|
+
if (/^[.#/\\]|[:\\]/u.test(row.name) || isBuiltin(row.name))
|
|
246
|
+
continue;
|
|
247
|
+
const parts = row.name.split('/');
|
|
248
|
+
const name = parts.slice(0, row.name.startsWith('@') ? 2 : 1).join('/');
|
|
249
|
+
if (!/^(?:@[\w.-]+\/)?[\w-][\w.-]*$/u.test(name) || checked.has(name))
|
|
250
|
+
continue;
|
|
251
|
+
checked.add(name);
|
|
252
|
+
const directory = resolveBundleRowPackage(packageDir, activeProfileDir, name);
|
|
253
|
+
if (directory === null)
|
|
254
|
+
continue;
|
|
255
|
+
const gate = peerGateAt(directory, name, facts);
|
|
256
|
+
if (gate !== null)
|
|
257
|
+
return { name, gate };
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
catch { /* malformed or cyclic patch data — no reliable prediction */ }
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Probe Node's search paths, without resolving an entry or requiring a
|
|
265
|
+
* package.json export. The real bundle path reaches pnpm's adjacent and
|
|
266
|
+
* nested dependencies; the profile anchor also covers workspace hoisting.
|
|
267
|
+
* Stop at a present directory even if its manifest is missing: a broken
|
|
268
|
+
* nearer copy must not borrow an incompatible manifest from a fallback.
|
|
269
|
+
*/
|
|
270
|
+
function resolveBundleRowPackage(packageDir, activeProfileDir, name) {
|
|
271
|
+
const bundleAnchor = join(realpathSync(packageDir), 'package.json');
|
|
272
|
+
const manifest = JSON.parse(readFileSync(bundleAnchor, 'utf8'));
|
|
273
|
+
// The host publishes dependencies carried by selected bundles into its
|
|
274
|
+
// profile resolution. A stray nested copy not declared by this bundle is
|
|
275
|
+
// no evidence of what a profile row will import.
|
|
276
|
+
const carried = manifest.name === name || Object.hasOwn(manifest.dependencies ?? {}, name)
|
|
277
|
+
|| Object.hasOwn(manifest.peerDependencies ?? {}, name);
|
|
278
|
+
const profileAnchor = join(activeProfileDir, 'package.json');
|
|
279
|
+
for (const anchor of carried ? [bundleAnchor, profileAnchor] : [profileAnchor]) {
|
|
280
|
+
for (const searchPath of createRequire(anchor).resolve.paths(name) ?? []) {
|
|
281
|
+
const candidate = join(searchPath, name);
|
|
282
|
+
try {
|
|
283
|
+
if (statSync(candidate).isDirectory())
|
|
284
|
+
return candidate;
|
|
285
|
+
}
|
|
286
|
+
catch { /* try the next Node search path */ }
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
218
291
|
/** The way out of a peer gate, in the direction the versions allow (#758). */
|
|
219
292
|
export function peerGateRemedy(gate) {
|
|
220
293
|
if (gate.hostTooOld) {
|
|
@@ -391,7 +464,8 @@ export function verifyActivation(profile, name, live = new Set(listHotMounts()),
|
|
|
391
464
|
// range excludes the running runtime — predict that verdict here so
|
|
392
465
|
// the plugin reads as incompatible instead of promising a restart
|
|
393
466
|
// that cannot fix it.
|
|
394
|
-
const
|
|
467
|
+
const facts = hostFacts ?? defaultHostRuntimeFacts(activeProfileDir);
|
|
468
|
+
const gate = hostPeerGate(activeProfileDir, name, facts);
|
|
395
469
|
if (gate !== null) {
|
|
396
470
|
const remedy = peerGateRemedy(gate);
|
|
397
471
|
return {
|
|
@@ -403,6 +477,21 @@ export function verifyActivation(profile, name, live = new Set(listHotMounts()),
|
|
|
403
477
|
hot: false,
|
|
404
478
|
};
|
|
405
479
|
}
|
|
480
|
+
const child = bundleRowPeerGate(dir, activeProfileDir, facts);
|
|
481
|
+
if (child !== null) {
|
|
482
|
+
const { gate, name: plugin } = child;
|
|
483
|
+
const remedy = gate.hostTooOld
|
|
484
|
+
? { zh: '升级 dsh,或更新此插件包以匹配当前 dsh', en: 'upgrade dsh, or update this bundle to match the current dsh' }
|
|
485
|
+
: { zh: '更新此插件包,或安装与当前 dsh 兼容的版本;升级 dsh 解决不了', en: 'update this bundle or install a version compatible with the current dsh — upgrading dsh will not help' };
|
|
486
|
+
return {
|
|
487
|
+
state: 'incompatible',
|
|
488
|
+
reasons: [
|
|
489
|
+
`它加载的插件 ${plugin} 要求 ${gate.peer} ${gate.range},当前 dsh 为 ${gate.runtimeVersion},重启也不会生效;${remedy.zh} / its plugin ${plugin} requires ${gate.peer} ${gate.range}, but dsh runs ${gate.runtimeVersion} — a restart will not activate it; ${remedy.en}`,
|
|
490
|
+
],
|
|
491
|
+
bundle: true,
|
|
492
|
+
hot: false,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
406
495
|
const patch = patchTextOf(profile, name, activeProfileDir);
|
|
407
496
|
const complex = patch !== null && parseSimplePatch(patch) === null;
|
|
408
497
|
return {
|
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.66.
|
|
4
|
+
"version": "1.66.24",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
44
|
-
"@deepseek-ai/dsh-settings": "^0.1.0-rc.7 || ^0.1.1-rc.2 || ^0.1.2-alpha.2 || ^0.2.0-rc.1",
|
|
44
|
+
"@deepseek-ai/dsh-settings": "^0.1.0-rc.7 || ^0.1.1-rc.2 || ^0.1.2-alpha.2 || ^0.2.0-rc.1 || ^0.2.1-alpha.3",
|
|
45
45
|
"@deepseek-ai/schemastery": "^3.18.1"
|
|
46
46
|
},
|
|
47
47
|
"dsh": {
|
package/src/changelog.ts
CHANGED
|
@@ -351,9 +351,10 @@ export interface UpdateNotes {
|
|
|
351
351
|
/**
|
|
352
352
|
* Present for `source`: `missing` when the notes loaded without this
|
|
353
353
|
* plugin, `unavailable` when they could not be loaded at all — a passing
|
|
354
|
-
* failure, not a fact about the plugin
|
|
354
|
+
* failure, not a fact about the plugin — and `unreadable` when the source is
|
|
355
|
+
* one the market can never read, so waiting will not help (#819).
|
|
355
356
|
*/
|
|
356
|
-
reason?: 'missing' | 'unavailable'
|
|
357
|
+
reason?: 'missing' | 'unavailable' | 'unreadable'
|
|
357
358
|
}
|
|
358
359
|
|
|
359
360
|
/**
|
|
@@ -380,9 +381,17 @@ export async function updateNotesFor(
|
|
|
380
381
|
const specKey = repoKeyOf(spec) ?? lookupRepoFromUrl(spec)
|
|
381
382
|
// The market holds no notes for this install, but its history is still
|
|
382
383
|
// readable where it came from: say which, and point there.
|
|
384
|
+
//
|
|
385
|
+
// Notes come from the catalog's own release feed, which is built from
|
|
386
|
+
// GitHub, so a gitee or self-hosted git install has no feed to find:
|
|
387
|
+
// "the market has not collected this one yet" would promise something that
|
|
388
|
+
// cannot happen, and the reader waits for it (#819). Those say the source
|
|
389
|
+
// cannot be read instead. The url is the same either way — the repository
|
|
390
|
+
// is where the answer actually is.
|
|
391
|
+
const unreadableSource = githubIdentityOf(spec) === null && isGitHostedSpec(spec)
|
|
383
392
|
const fromSource = (reason: 'missing' | 'unavailable'): UpdateNotes => ({
|
|
384
393
|
kind: 'source',
|
|
385
|
-
reason,
|
|
394
|
+
reason: unreadableSource ? 'unreadable' : reason,
|
|
386
395
|
sourceUrl: sourceWebUrl(spec) ?? specKey,
|
|
387
396
|
})
|
|
388
397
|
try {
|
|
@@ -1996,13 +1996,13 @@ export function MarketSection(props: MarketSectionProps) {
|
|
|
1996
1996
|
commits?: { items: NoteCommit[]; found: boolean }
|
|
1997
1997
|
npmTimes?: Array<{ version: string; date: string }>
|
|
1998
1998
|
sourceUrl?: string | null
|
|
1999
|
-
reason?: 'missing' | 'unavailable'
|
|
1999
|
+
reason?: 'missing' | 'unavailable' | 'unreadable'
|
|
2000
2000
|
}
|
|
2001
2001
|
type ResolvedNotes =
|
|
2002
2002
|
| { kind: 'release'; release: NoteRelease }
|
|
2003
2003
|
| { kind: 'commits'; commits: { items: NoteCommit[]; found: boolean } }
|
|
2004
2004
|
| { kind: 'npm'; npmTimes: Array<{ version: string; date: string }> }
|
|
2005
|
-
| { kind: 'source'; sourceUrl?: string | null; reason?: 'missing' | 'unavailable' }
|
|
2005
|
+
| { kind: 'source'; sourceUrl?: string | null; reason?: 'missing' | 'unavailable' | 'unreadable' }
|
|
2006
2006
|
| { kind: 'none' }
|
|
2007
2007
|
interface CompatibilityNotice {
|
|
2008
2008
|
code: 'soft-incompatible'
|
|
@@ -2279,6 +2279,7 @@ export function MarketSection(props: MarketSectionProps) {
|
|
|
2279
2279
|
const [restarting, setRestarting] = useState(false)
|
|
2280
2280
|
const [showTop, setShowTop] = useState(false)
|
|
2281
2281
|
const [backupBusy, setBackupBusy] = useState(false)
|
|
2282
|
+
const webdavInFlight = useRef(false)
|
|
2282
2283
|
const [backupMessage, setBackupMessage] = useState<string | null>(null)
|
|
2283
2284
|
const [backupRestored, setBackupRestored] = useState(false)
|
|
2284
2285
|
const [pendingBackup, setPendingBackup] = useState<unknown>(null)
|
|
@@ -4462,7 +4463,23 @@ export function MarketSection(props: MarketSectionProps) {
|
|
|
4462
4463
|
// The market manages itself from its own settings card (Settings → Plugins
|
|
4463
4464
|
// → Plugin configuration), not as a row here — listing it in both places
|
|
4464
4465
|
// read as two different controls for the same thing.
|
|
4465
|
-
|
|
4466
|
+
/**
|
|
4467
|
+
* Installed packages that are not plugins at all: a plain library some
|
|
4468
|
+
* other plugin pulled in — zustand, bufferutil, a peer of ws (#634's
|
|
4469
|
+
* `dependencyOf`, #857). They were rendered as plugin cards with a
|
|
4470
|
+
* working uninstall, which is a real remove path for something a plugin
|
|
4471
|
+
* needs. Out of the list; the count stays visible in one quiet line, so the
|
|
4472
|
+
* list is not silently shorter than package.json.
|
|
4473
|
+
*/
|
|
4474
|
+
const dependencyOnlyNames = useMemo(
|
|
4475
|
+
() => new Set(Object.keys(installed).filter(name => {
|
|
4476
|
+
const act = activations[name]
|
|
4477
|
+
return act?.state === 'inert' && act.dependencyOf !== undefined
|
|
4478
|
+
})),
|
|
4479
|
+
[installed, activations],
|
|
4480
|
+
)
|
|
4481
|
+
const installedOtherCount = Object.keys(installed)
|
|
4482
|
+
.filter(name => name !== selfName && !dependencyOnlyNames.has(name)).length
|
|
4466
4483
|
|
|
4467
4484
|
const ignoreUpdateNotices = useCallback((names: string[]) => {
|
|
4468
4485
|
if (bootId === null || names.length === 0) return
|
|
@@ -4560,7 +4577,8 @@ export function MarketSection(props: MarketSectionProps) {
|
|
|
4560
4577
|
}, [finishRestore, pendingBackup])
|
|
4561
4578
|
|
|
4562
4579
|
const runWebdav = useCallback((action: 'backup' | 'restore') => {
|
|
4563
|
-
if (webdavUrl.trim() === '') return
|
|
4580
|
+
if (webdavUrl.trim() === '' || webdavInFlight.current) return
|
|
4581
|
+
webdavInFlight.current = true
|
|
4564
4582
|
setBackupBusy(true)
|
|
4565
4583
|
setBackupMessage(null)
|
|
4566
4584
|
setRestoreErrors([])
|
|
@@ -4577,7 +4595,10 @@ export function MarketSection(props: MarketSectionProps) {
|
|
|
4577
4595
|
try { localStorage.setItem('dshm-webdav-last', String(Date.now())) } catch { /* storage unavailable */ }
|
|
4578
4596
|
setBackupMessage(t('backupDone'))
|
|
4579
4597
|
}
|
|
4580
|
-
}).catch(error => setBackupMessage(String(error))).finally(() =>
|
|
4598
|
+
}).catch(error => setBackupMessage(String(error))).finally(() => {
|
|
4599
|
+
webdavInFlight.current = false
|
|
4600
|
+
setBackupBusy(false)
|
|
4601
|
+
})
|
|
4581
4602
|
}, [previewBackup, t, webdavPassword, webdavUrl, webdavUser])
|
|
4582
4603
|
|
|
4583
4604
|
/** Map the server's token-source string to a localized label. */
|
|
@@ -4702,6 +4723,9 @@ export function MarketSection(props: MarketSectionProps) {
|
|
|
4702
4723
|
setExportOpen(true)
|
|
4703
4724
|
}, [installed, installedBundles])
|
|
4704
4725
|
|
|
4726
|
+
const runWebdavRef = useRef(runWebdav)
|
|
4727
|
+
useEffect(() => { runWebdavRef.current = runWebdav }, [runWebdav])
|
|
4728
|
+
|
|
4705
4729
|
useEffect(() => {
|
|
4706
4730
|
// Persist only the non-secret WebDAV settings; the password stays
|
|
4707
4731
|
// server-side/in-memory (see savedWebdav). Storage itself may be
|
|
@@ -4709,13 +4733,18 @@ export function MarketSection(props: MarketSectionProps) {
|
|
|
4709
4733
|
try {
|
|
4710
4734
|
localStorage.setItem(WEBDAV_STORAGE_KEY, JSON.stringify({ url: webdavUrl, username: webdavUser, auto: autoBackup }))
|
|
4711
4735
|
} catch { /* storage unavailable — config just won't survive reload */ }
|
|
4712
|
-
|
|
4736
|
+
}, [autoBackup, webdavUrl, webdavUser])
|
|
4737
|
+
|
|
4738
|
+
// Check when the market opens or daily backup is enabled. Editing the
|
|
4739
|
+
// settings (or changing the locale) must not start another upload.
|
|
4740
|
+
useEffect(() => {
|
|
4741
|
+
if (!autoBackup) return
|
|
4713
4742
|
let last = 0
|
|
4714
4743
|
try {
|
|
4715
4744
|
last = Number(localStorage.getItem('dshm-webdav-last')) || 0
|
|
4716
4745
|
} catch { /* ignore */ }
|
|
4717
|
-
if (Date.now() - last >= 24 * 60 * 60 * 1000)
|
|
4718
|
-
}, [autoBackup
|
|
4746
|
+
if (Date.now() - last >= 24 * 60 * 60 * 1000) runWebdavRef.current('backup')
|
|
4747
|
+
}, [autoBackup])
|
|
4719
4748
|
|
|
4720
4749
|
// #558: count restart-pending session changes (restartNames), not every
|
|
4721
4750
|
// completed `updatedNames` entry (which would double-count a batch update
|
|
@@ -4759,6 +4788,7 @@ export function MarketSection(props: MarketSectionProps) {
|
|
|
4759
4788
|
const orderedInstalledEntries = useMemo(() => {
|
|
4760
4789
|
return Object.entries(displayedInstalled)
|
|
4761
4790
|
.filter(([name]) => name !== selfName)
|
|
4791
|
+
.filter(([name]) => !dependencyOnlyNames.has(name))
|
|
4762
4792
|
.sort(([nameA, specA], [nameB, specB]) => {
|
|
4763
4793
|
// The list reads as "what is running", so a plugin that is off sinks
|
|
4764
4794
|
// below every one that is on (#745) — even a disabled row with a
|
|
@@ -4771,7 +4801,7 @@ export function MarketSection(props: MarketSectionProps) {
|
|
|
4771
4801
|
return bUp - aUp
|
|
4772
4802
|
})
|
|
4773
4803
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- `updatesLoaded` stands in for `updates`/`updatedNames`: reorder when the check lands, then hold (#631). `effectiveDisabledSet` only re-runs this when a disable state actually changed; the toggled row is the only one that moves.
|
|
4774
|
-
}, [isInstalledListActive, displayedInstalled, selfName, updatesLoaded, effectiveDisabledSet])
|
|
4804
|
+
}, [isInstalledListActive, displayedInstalled, selfName, updatesLoaded, effectiveDisabledSet, dependencyOnlyNames])
|
|
4775
4805
|
const missingRestoreCount = Object.keys(pendingDependencies).filter(name => !installedFiles.includes(name)).length
|
|
4776
4806
|
// Self-update lives in the header button and the settings card, not this
|
|
4777
4807
|
// tab's row list (the market itself is filtered out below) — so a pending
|
|
@@ -5520,7 +5550,8 @@ export function MarketSection(props: MarketSectionProps) {
|
|
|
5520
5550
|
)
|
|
5521
5551
|
|
|
5522
5552
|
/** Installed plugins the market itself cannot group (#60). */
|
|
5523
|
-
const groupableNames = Object.keys(installed)
|
|
5553
|
+
const groupableNames = Object.keys(installed)
|
|
5554
|
+
.filter(name => name !== 'dsh-market' && name !== 'dshmarket' && !dependencyOnlyNames.has(name))
|
|
5524
5555
|
/** Names already inside some group; everything else shows under "ungrouped". */
|
|
5525
5556
|
const groupedNames = useMemo(() => new Set(Object.values(groups).flat()), [groups])
|
|
5526
5557
|
const ungroupedNames = groupableNames.filter(name => !groupedNames.has(name))
|
|
@@ -6049,6 +6080,12 @@ export function MarketSection(props: MarketSectionProps) {
|
|
|
6049
6080
|
]}
|
|
6050
6081
|
/>
|
|
6051
6082
|
<Input className={css.backupInput} icon={<IconLinkOutline14 size={14} />} type="url" value={webdavUrl} placeholder={t('webdavUrl')} onChange={e => setWebdavUrl(e.target.value)} />
|
|
6083
|
+
{/* The field had a placeholder and nothing else, so "is this a
|
|
6084
|
+
folder or a file?" was answered only by trying it — and a
|
|
6085
|
+
reporter who read it as a folder expected the market to
|
|
6086
|
+
name the file (#868). Name the shape, and say why the
|
|
6087
|
+
name has to stay put: restore looks for that same one. */}
|
|
6088
|
+
<p>{t('webdavPathHint')}</p>
|
|
6052
6089
|
<Input className={css.backupInput} autoComplete="username" value={webdavUser} placeholder={t('webdavUser')} onChange={e => setWebdavUser(e.target.value)} />
|
|
6053
6090
|
<Input className={css.backupInput} type="password" autoComplete="current-password" value={webdavPassword} placeholder={t('webdavPassword')} onChange={e => setWebdavPassword(e.target.value)} />
|
|
6054
6091
|
<div className={css.backupActions}>
|
|
@@ -7045,6 +7082,13 @@ export function MarketSection(props: MarketSectionProps) {
|
|
|
7045
7082
|
}}
|
|
7046
7083
|
/>
|
|
7047
7084
|
)}
|
|
7085
|
+
{dependencyOnlyNames.size > 0 && (
|
|
7086
|
+
// The list is complete as a list of PLUGINS; the count keeps it
|
|
7087
|
+
// from looking like packages went missing (#857).
|
|
7088
|
+
<p className={css.groupOrgHint}>
|
|
7089
|
+
{t('installedDependencyNote').replace('{0}', String(dependencyOnlyNames.size))}
|
|
7090
|
+
</p>
|
|
7091
|
+
)}
|
|
7048
7092
|
</>
|
|
7049
7093
|
)}
|
|
7050
7094
|
</div>
|
|
@@ -7606,7 +7650,9 @@ export function MarketSection(props: MarketSectionProps) {
|
|
|
7606
7650
|
)
|
|
7607
7651
|
: updateNotes.kind === 'source' ? (
|
|
7608
7652
|
<div className={css.spec}>
|
|
7609
|
-
{t(updateNotes.reason === 'unavailable'
|
|
7653
|
+
{t(updateNotes.reason === 'unavailable'
|
|
7654
|
+
? 'notesLoadFail'
|
|
7655
|
+
: updateNotes.reason === 'unreadable' ? 'notesSourceUnreadable' : 'notesSource')}
|
|
7610
7656
|
{typeof updateNotes.sourceUrl === 'string' && /^https?:\/\//i.test(updateNotes.sourceUrl)
|
|
7611
7657
|
? <>{lang === 'zh' ? '' : ' '}<a className={css.notesSourceLink} href={updateNotes.sourceUrl} target="_blank" rel="noreferrer">{t('notesSourceLink')}</a></>
|
|
7612
7658
|
: updateNotes.reason !== 'unavailable' && <>{lang === 'zh' ? '' : ' '}{t('notesSourceNoLink')}</>}
|
package/src/client/locales.ts
CHANGED
|
@@ -198,6 +198,7 @@ export const zh = {
|
|
|
198
198
|
notesNpm: '版本发布时间',
|
|
199
199
|
notesNone: '该插件未提供更新说明。',
|
|
200
200
|
notesSource: '市场还没收录这个插件的更新说明。',
|
|
201
|
+
notesSourceUnreadable: '这个插件不是从 GitHub 装的,市场读不到它的改动记录。',
|
|
201
202
|
notesSourceLink: '去仓库看改了什么',
|
|
202
203
|
notesSourceNoLink: '可以问问插件作者改了什么。',
|
|
203
204
|
notesLoadFail: '暂时读不到更新说明,稍后再试。',
|
|
@@ -366,6 +367,7 @@ export const zh = {
|
|
|
366
367
|
marketUpdate: '更新插件市场',
|
|
367
368
|
updateAll: '全部更新',
|
|
368
369
|
heldReleaseNotice: '已安装,但不是最新版:这个 profile 的 minimumReleaseAge 挡住了 {0},安装的是 {1}。',
|
|
370
|
+
installedDependencyNote: '另有 {0} 个包是插件带进来的依赖,不是插件,未列在上方。',
|
|
369
371
|
heldReleaseAction: '仍安装 {0}',
|
|
370
372
|
ignoreUpdateNotice: '重启前不再提醒',
|
|
371
373
|
ignoreAllUpdateNotices: '重启前都不再提醒',
|
|
@@ -397,6 +399,7 @@ export const zh = {
|
|
|
397
399
|
webdavRestore: '从 WebDAV 恢复',
|
|
398
400
|
autoBackup: '每天自动备份(打开市场时)',
|
|
399
401
|
webdavNote: 'WebDAV 地址与用户名仅保存在当前浏览器;密码只存于服务端,每次会话需重新输入。',
|
|
402
|
+
webdavPathHint: '填完整的文件地址(含文件名):备份写进这个文件,恢复也从它读。开启自动备份后每天最多上传一次。',
|
|
400
403
|
localOnly: 'WebDAV 地址和用户名只保存在此浏览器。',
|
|
401
404
|
credsWarning: '请注意:备份包含 profile 配置,以及可能含密钥的文件(config.toml、.env 等)。本地导出的文件会原样包含这些内容,上传 WebDAV 前请确认目标可信。',
|
|
402
405
|
gist: 'GitHub Gist',
|
|
@@ -934,6 +937,7 @@ export const en: Record<MarketKey, string> = {
|
|
|
934
937
|
notesNpm: 'Version publish times',
|
|
935
938
|
notesNone: 'This plugin does not publish update notes.',
|
|
936
939
|
notesSource: "The market doesn't have update notes for this plugin yet.",
|
|
940
|
+
notesSourceUnreadable: 'This plugin was not installed from GitHub, so the market cannot read its change history.',
|
|
937
941
|
notesSourceLink: 'See what changed in the repository',
|
|
938
942
|
notesSourceNoLink: 'You can ask the plugin author what changed.',
|
|
939
943
|
notesLoadFail: 'Update notes are unavailable right now — try again later.',
|
|
@@ -1096,6 +1100,7 @@ export const en: Record<MarketKey, string> = {
|
|
|
1096
1100
|
marketUpdate: 'Update the plugin market',
|
|
1097
1101
|
updateAll: 'Update all',
|
|
1098
1102
|
heldReleaseNotice: 'Installed, but not the newest: this profile\'s minimumReleaseAge held {0} back, so {1} was installed.',
|
|
1103
|
+
installedDependencyNote: '{0} more packages came in as dependencies of plugins — they are not plugins, so they are not listed above.',
|
|
1099
1104
|
heldReleaseAction: 'Install {0} anyway',
|
|
1100
1105
|
ignoreUpdateNotice: "Don't remind until restart",
|
|
1101
1106
|
ignoreAllUpdateNotices: "Don't remind any until restart",
|
|
@@ -1127,6 +1132,7 @@ export const en: Record<MarketKey, string> = {
|
|
|
1127
1132
|
webdavRestore: 'Restore from WebDAV',
|
|
1128
1133
|
autoBackup: 'Back up daily (when the market opens)',
|
|
1129
1134
|
webdavNote: 'The WebDAV URL and username stay in this browser only; the password is kept server-side and must be re-entered each session.',
|
|
1135
|
+
webdavPathHint: 'Give the whole file path, name included: the backup writes that file and restore reads it back. With automatic backup on it uploads at most once a day.',
|
|
1130
1136
|
localOnly: 'The WebDAV URL and username stay in this browser only.',
|
|
1131
1137
|
credsWarning: 'Note: backups include profile configuration and files that may hold secrets (config.toml, .env, …). Local exports are unmodified, so upload only to a WebDAV target you trust.',
|
|
1132
1138
|
gist: 'GitHub Gist',
|
package/src/install.ts
CHANGED
|
@@ -334,10 +334,16 @@ export async function withHoistRecovery(
|
|
|
334
334
|
// no options from us — but the classified message above says the market
|
|
335
335
|
// retried. Say that it did not, or the user reads a retry that never
|
|
336
336
|
// happened and looks for the wrong cause.
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
337
|
+
//
|
|
338
|
+
// The wait on a fresh release is the one of these where "no retry" leaves
|
|
339
|
+
// the user with nothing to do at all: the plugin IS published, pnpm just
|
|
340
|
+
// will not take it yet, and the door the market would open on a plain
|
|
341
|
+
// `dsh web` host is shut here — so say when the same button starts
|
|
342
|
+
// working, and what to use meanwhile (#852).
|
|
343
|
+
const note = unavailableOption === RELEASE_AGE_OVERRIDE
|
|
344
|
+
? `这个插件的新版本比本 profile 的 minimumReleaseAge(新版本等待期)还新,pnpm 因此拒绝了本次改动(改别的插件也一样)。官方桌面端由应用自己执行安装,不接受市场的临时放行参数(${unavailableOption}),所以这里没有自动重试。**等这个版本过了等待期再更新一次即可**;想现在就更新,可以在普通的 dsh web 里装。 / The plugin's new version is younger than this profile's minimumReleaseAge, so pnpm refused this change (a change to another plugin too). The official desktop app runs the install itself and takes no one-shot bypass from the market (${unavailableOption}), so no automatic retry happens here. **Update again once the version is past the wait**; to update right now, install from plain dsh web.`
|
|
345
|
+
: `这台宿主(官方桌面端)自己执行安装,不接受市场附加的 pnpm 参数(${unavailableOption}),所以这一步没有自动重试。 / This host (the official desktop app) runs the install itself and takes no pnpm options from the market (${unavailableOption}), so that automatic retry could not be attempted.`
|
|
346
|
+
result = { ...result, stderr: `${result.stderr}\n\n${note}` }
|
|
341
347
|
}
|
|
342
348
|
return result
|
|
343
349
|
}
|
|
@@ -432,7 +438,25 @@ export function failureDetail(result: InstallResult, limit = 300): string {
|
|
|
432
438
|
const code = result.pnpmErrorCode === undefined ? '' : `${result.pnpmErrorCode}: `
|
|
433
439
|
return `${code}${result.pnpmError}`.slice(0, limit)
|
|
434
440
|
}
|
|
435
|
-
|
|
441
|
+
const text = (result.stderr || result.stdout).trim()
|
|
442
|
+
if (text.length <= limit) return text
|
|
443
|
+
const tail = text.slice(-limit)
|
|
444
|
+
// Cut at a sentence end instead of mid-word, and say that something was
|
|
445
|
+
// dropped (#852). The market appends its own explanation to pnpm's output,
|
|
446
|
+
// and this tail is catalogued into user-facing sentences — including inside
|
|
447
|
+
// a parenthesis. A blunt cut landed inside the previous sentence and welded
|
|
448
|
+
// the fragment to the next one: the reporter's log read
|
|
449
|
+
// `(is, export the log and report it这台宿主…`, which is not a sentence in
|
|
450
|
+
// either language and says nothing about what to do.
|
|
451
|
+
const sentenceEnd = /[.!?。!?]["')\])]?[\s\n]+/gu
|
|
452
|
+
const match = sentenceEnd.exec(tail)
|
|
453
|
+
if (match !== null) {
|
|
454
|
+
const rest = tail.slice(match.index + match[0].length).trim()
|
|
455
|
+
// A boundary so near the end would leave a word or two; the raw tail is
|
|
456
|
+
// more use than the stub.
|
|
457
|
+
if (rest.length >= 40) return `… ${rest}`
|
|
458
|
+
}
|
|
459
|
+
return `…${tail.trim()}`
|
|
436
460
|
}
|
|
437
461
|
|
|
438
462
|
/**
|
package/src/verify.ts
CHANGED
|
@@ -27,14 +27,15 @@
|
|
|
27
27
|
* missing – not present in node_modules
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
-
import { readFileSync } from 'node:fs'
|
|
30
|
+
import { readFileSync, realpathSync, statSync } from 'node:fs'
|
|
31
|
+
import { createRequire, isBuiltin } from 'node:module'
|
|
31
32
|
import { Script } from 'node:vm'
|
|
32
33
|
import { join } from 'node:path'
|
|
33
34
|
import { listHotMounts, parseSimplePatch } from './hot.ts'
|
|
34
35
|
import { userPatchPackageReferences } from './patch.ts'
|
|
35
36
|
import { nameMatchesPackage } from './entry-identity.ts'
|
|
36
37
|
import { bundlePatchInsertedIds, declaredBundlePatchFiles, hasDshManifest, hasLoadableEntry, profileDir, readInstalled } from './profile.ts'
|
|
37
|
-
import { compareSemver, parsePatchFile, satisfiesRange } from './check.ts'
|
|
38
|
+
import { compareSemver, composeLayers, parsePatchFile, satisfiesRange } from './check.ts'
|
|
38
39
|
import { dshHostInfo } from './dsh-install.ts'
|
|
39
40
|
|
|
40
41
|
export type ActivationState = 'live' | 'restart' | 'incompatible' | 'inert' | 'broken' | 'missing' | 'disabled'
|
|
@@ -247,11 +248,16 @@ const isDshPeer = (peer: string): boolean => peer === '@deepseek-ai/dsh' || peer
|
|
|
247
248
|
* peers fail at once.
|
|
248
249
|
*/
|
|
249
250
|
export function hostPeerGate(activeProfileDir: string, name: string, facts: HostRuntimeFacts): PeerGate | null {
|
|
251
|
+
return peerGateAt(join(activeProfileDir, 'node_modules', name), name, facts)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Same prediction for a patch plugin resolved outside the profile's flat node_modules. */
|
|
255
|
+
function peerGateAt(packageDir: string, name: string, facts: HostRuntimeFacts): PeerGate | null {
|
|
250
256
|
if (facts.runtimeVersion === null) return null
|
|
251
257
|
let manifest: { version?: unknown; peerDependencies?: Record<string, unknown> }
|
|
252
258
|
try {
|
|
253
259
|
manifest = JSON.parse(
|
|
254
|
-
readFileSync(join(
|
|
260
|
+
readFileSync(join(packageDir, 'package.json'), 'utf8'),
|
|
255
261
|
)
|
|
256
262
|
} catch {
|
|
257
263
|
return null
|
|
@@ -278,6 +284,72 @@ export function hostPeerGate(activeProfileDir: string, name: string, facts: Host
|
|
|
278
284
|
return null
|
|
279
285
|
}
|
|
280
286
|
|
|
287
|
+
/**
|
|
288
|
+
* #867: only the bundle's own effective loader rows are evidence, never its
|
|
289
|
+
* ordinary dependency closure or arbitrary names in plugin configuration.
|
|
290
|
+
* Compose every declared file together so later disables take effect. An
|
|
291
|
+
* unreadable file or an expression-gated row gives no rejection evidence.
|
|
292
|
+
*/
|
|
293
|
+
function bundleRowPeerGate(
|
|
294
|
+
packageDir: string,
|
|
295
|
+
activeProfileDir: string,
|
|
296
|
+
facts: HostRuntimeFacts,
|
|
297
|
+
): { name: string; gate: PeerGate } | null {
|
|
298
|
+
if (facts.runtimeVersion === null) return null
|
|
299
|
+
try {
|
|
300
|
+
const patches = declaredBundlePatchFiles(packageDir).map(parsePatchFile)
|
|
301
|
+
if (patches.some(rows => rows === null)) return null
|
|
302
|
+
const { resolvableRows } = composeLayers([{
|
|
303
|
+
label: 'bundle', kind: 'bundle', patches: patches.flatMap(rows => rows ?? []), parseError: null,
|
|
304
|
+
}])
|
|
305
|
+
const checked = new Set<string>()
|
|
306
|
+
for (const row of resolvableRows) {
|
|
307
|
+
if (row.activation !== 'required' || typeof row.name !== 'string') continue
|
|
308
|
+
// Bare npm names and subpath entries only. Relative files, builtins,
|
|
309
|
+
// aliases and expressions need runtime resolution; do not guess.
|
|
310
|
+
if (/^[.#/\\]|[:\\]/u.test(row.name) || isBuiltin(row.name)) continue
|
|
311
|
+
const parts = row.name.split('/')
|
|
312
|
+
const name = parts.slice(0, row.name.startsWith('@') ? 2 : 1).join('/')
|
|
313
|
+
if (!/^(?:@[\w.-]+\/)?[\w-][\w.-]*$/u.test(name) || checked.has(name)) continue
|
|
314
|
+
checked.add(name)
|
|
315
|
+
const directory = resolveBundleRowPackage(packageDir, activeProfileDir, name)
|
|
316
|
+
if (directory === null) continue
|
|
317
|
+
const gate = peerGateAt(directory, name, facts)
|
|
318
|
+
if (gate !== null) return { name, gate }
|
|
319
|
+
}
|
|
320
|
+
} catch { /* malformed or cyclic patch data — no reliable prediction */ }
|
|
321
|
+
return null
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Probe Node's search paths, without resolving an entry or requiring a
|
|
326
|
+
* package.json export. The real bundle path reaches pnpm's adjacent and
|
|
327
|
+
* nested dependencies; the profile anchor also covers workspace hoisting.
|
|
328
|
+
* Stop at a present directory even if its manifest is missing: a broken
|
|
329
|
+
* nearer copy must not borrow an incompatible manifest from a fallback.
|
|
330
|
+
*/
|
|
331
|
+
function resolveBundleRowPackage(packageDir: string, activeProfileDir: string, name: string): string | null {
|
|
332
|
+
const bundleAnchor = join(realpathSync(packageDir), 'package.json')
|
|
333
|
+
const manifest = JSON.parse(readFileSync(bundleAnchor, 'utf8')) as {
|
|
334
|
+
name?: string; dependencies?: Record<string, unknown>; peerDependencies?: Record<string, unknown>
|
|
335
|
+
}
|
|
336
|
+
// The host publishes dependencies carried by selected bundles into its
|
|
337
|
+
// profile resolution. A stray nested copy not declared by this bundle is
|
|
338
|
+
// no evidence of what a profile row will import.
|
|
339
|
+
const carried = manifest.name === name || Object.hasOwn(manifest.dependencies ?? {}, name)
|
|
340
|
+
|| Object.hasOwn(manifest.peerDependencies ?? {}, name)
|
|
341
|
+
const profileAnchor = join(activeProfileDir, 'package.json')
|
|
342
|
+
for (const anchor of carried ? [bundleAnchor, profileAnchor] : [profileAnchor]) {
|
|
343
|
+
for (const searchPath of createRequire(anchor).resolve.paths(name) ?? []) {
|
|
344
|
+
const candidate = join(searchPath, name)
|
|
345
|
+
try {
|
|
346
|
+
if (statSync(candidate).isDirectory()) return candidate
|
|
347
|
+
} catch { /* try the next Node search path */ }
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return null
|
|
351
|
+
}
|
|
352
|
+
|
|
281
353
|
/** The way out of a peer gate, in the direction the versions allow (#758). */
|
|
282
354
|
export function peerGateRemedy(gate: PeerGate): { zh: string; en: string } {
|
|
283
355
|
if (gate.hostTooOld) {
|
|
@@ -467,7 +539,8 @@ export function verifyActivation(
|
|
|
467
539
|
// range excludes the running runtime — predict that verdict here so
|
|
468
540
|
// the plugin reads as incompatible instead of promising a restart
|
|
469
541
|
// that cannot fix it.
|
|
470
|
-
const
|
|
542
|
+
const facts = hostFacts ?? defaultHostRuntimeFacts(activeProfileDir)
|
|
543
|
+
const gate = hostPeerGate(activeProfileDir, name, facts)
|
|
471
544
|
if (gate !== null) {
|
|
472
545
|
const remedy = peerGateRemedy(gate)
|
|
473
546
|
return {
|
|
@@ -479,6 +552,21 @@ export function verifyActivation(
|
|
|
479
552
|
hot: false,
|
|
480
553
|
}
|
|
481
554
|
}
|
|
555
|
+
const child = bundleRowPeerGate(dir, activeProfileDir, facts)
|
|
556
|
+
if (child !== null) {
|
|
557
|
+
const { gate, name: plugin } = child
|
|
558
|
+
const remedy = gate.hostTooOld
|
|
559
|
+
? { zh: '升级 dsh,或更新此插件包以匹配当前 dsh', en: 'upgrade dsh, or update this bundle to match the current dsh' }
|
|
560
|
+
: { zh: '更新此插件包,或安装与当前 dsh 兼容的版本;升级 dsh 解决不了', en: 'update this bundle or install a version compatible with the current dsh — upgrading dsh will not help' }
|
|
561
|
+
return {
|
|
562
|
+
state: 'incompatible',
|
|
563
|
+
reasons: [
|
|
564
|
+
`它加载的插件 ${plugin} 要求 ${gate.peer} ${gate.range},当前 dsh 为 ${gate.runtimeVersion},重启也不会生效;${remedy.zh} / its plugin ${plugin} requires ${gate.peer} ${gate.range}, but dsh runs ${gate.runtimeVersion} — a restart will not activate it; ${remedy.en}`,
|
|
565
|
+
],
|
|
566
|
+
bundle: true,
|
|
567
|
+
hot: false,
|
|
568
|
+
}
|
|
569
|
+
}
|
|
482
570
|
const patch = patchTextOf(profile, name, activeProfileDir)
|
|
483
571
|
const complex = patch !== null && parseSimplePatch(patch) === null
|
|
484
572
|
return {
|