dshmarket 1.66.25 → 1.66.27
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 +131 -25
- package/lib/check.js +29 -2
- package/lib/verify.js +35 -11
- package/package.json +1 -1
- package/src/check.ts +27 -2
- package/src/client/MarketSection.tsx +59 -13
- package/src/client/locales.ts +4 -2
- package/src/client/market-data.ts +36 -0
- package/src/verify.ts +39 -11
package/client/client.js
CHANGED
|
@@ -510,6 +510,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
510
510
|
confirmInstall: "确认安装",
|
|
511
511
|
cmdDetails: "安装命令",
|
|
512
512
|
cmdCopy: "复制安装命令",
|
|
513
|
+
cmdDesktopUnavailable: "桌面端的插件由应用自己安装(用上面的「安装」按钮);`dsh plugin` 不接受桌面端这个 profile。",
|
|
513
514
|
cmdCopied: "已复制",
|
|
514
515
|
catsMore: "更多分类",
|
|
515
516
|
catsLess: "收起",
|
|
@@ -523,7 +524,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
523
524
|
hostDetecting: "正在识别宿主版本…",
|
|
524
525
|
hostUnknown: "宿主版本未知",
|
|
525
526
|
hostFilterLoading: "正在检查宿主要求 {0}/{1};未声明或无法确认的插件仍会显示",
|
|
526
|
-
hostFilterActive: "
|
|
527
|
+
hostFilterActive: "已隐藏 {1} 个与 DSH {0} 不兼容的插件;未声明版本的仍在列表里",
|
|
527
528
|
hostRequirement: "DSH {0}",
|
|
528
529
|
hostRequirementLoading: "正在读取宿主要求…",
|
|
529
530
|
hostRequirementUndeclared: "未声明宿主要求",
|
|
@@ -1238,6 +1239,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
1238
1239
|
confirmInstall: "Confirm install",
|
|
1239
1240
|
cmdDetails: "Install command",
|
|
1240
1241
|
cmdCopy: "Copy install command",
|
|
1242
|
+
cmdDesktopUnavailable: "The desktop app installs plugins itself — use the Install button above; `dsh plugin` refuses this profile.",
|
|
1241
1243
|
cmdCopied: "Copied",
|
|
1242
1244
|
catsMore: "More categories",
|
|
1243
1245
|
catsLess: "Fewer categories",
|
|
@@ -1251,7 +1253,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
1251
1253
|
hostDetecting: "Detecting host version…",
|
|
1252
1254
|
hostUnknown: "Host version unknown",
|
|
1253
1255
|
hostFilterLoading: "Checking host requirements {0}/{1}; undeclared or unavailable plugins remain visible",
|
|
1254
|
-
hostFilterActive: "
|
|
1256
|
+
hostFilterActive: "Hid {1} plugins incompatible with DSH {0}; those declaring no version stay listed",
|
|
1255
1257
|
hostRequirement: "DSH {0}",
|
|
1256
1258
|
hostRequirementLoading: "Reading host requirement…",
|
|
1257
1259
|
hostRequirementUndeclared: "Host requirement undeclared",
|
|
@@ -1901,6 +1903,53 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
1901
1903
|
if (state.supervisor !== null) return "restartHintSupervised";
|
|
1902
1904
|
return "restartHint";
|
|
1903
1905
|
}
|
|
1906
|
+
/**
|
|
1907
|
+
* The install command shown on a plugin's card, corrected for THIS profile
|
|
1908
|
+
* (#872).
|
|
1909
|
+
*
|
|
1910
|
+
* awesome-dsh-plugin generates the string once, for the catalog — always
|
|
1911
|
+
* `dsh plugin --profile web add <spec>` — which is right on a `web` profile
|
|
1912
|
+
* and wrong on every other one. On a desktop profile it is more than wrong:
|
|
1913
|
+
* the DSH CLI refuses a profile by that name, so the command as printed
|
|
1914
|
+
* cannot install anything there at all. Two outcomes, and the caller renders
|
|
1915
|
+
* them differently because the user's next step differs:
|
|
1916
|
+
*
|
|
1917
|
+
* - `command`: the text to show and copy, with the profile it names corrected;
|
|
1918
|
+
* - `unusable`: show no command at all (the market's own button is the way),
|
|
1919
|
+
* because handing over a command that cannot work is worse than handing
|
|
1920
|
+
* over none.
|
|
1921
|
+
*
|
|
1922
|
+
* A command with no `--profile` token is passed through untouched: it is not
|
|
1923
|
+
* this function's business to add one.
|
|
1924
|
+
*/
|
|
1925
|
+
function installCommandFor(command, profile) {
|
|
1926
|
+
if (typeof command !== "string" || command.trim() === "") return {
|
|
1927
|
+
command: null,
|
|
1928
|
+
unusable: false
|
|
1929
|
+
};
|
|
1930
|
+
const named = /--profile(?:=|\s+)(\S+)/u.exec(command);
|
|
1931
|
+
if (named === null) return {
|
|
1932
|
+
command,
|
|
1933
|
+
unusable: false
|
|
1934
|
+
};
|
|
1935
|
+
const wanted = typeof profile === "string" ? profile.trim() : "";
|
|
1936
|
+
if (wanted === "") return {
|
|
1937
|
+
command,
|
|
1938
|
+
unusable: false
|
|
1939
|
+
};
|
|
1940
|
+
if (wanted.toLowerCase() === "desktop") return {
|
|
1941
|
+
command: null,
|
|
1942
|
+
unusable: true
|
|
1943
|
+
};
|
|
1944
|
+
if (named[1] === wanted) return {
|
|
1945
|
+
command,
|
|
1946
|
+
unusable: false
|
|
1947
|
+
};
|
|
1948
|
+
return {
|
|
1949
|
+
command: command.replace(named[0], `--profile ${wanted}`),
|
|
1950
|
+
unusable: false
|
|
1951
|
+
};
|
|
1952
|
+
}
|
|
1904
1953
|
function groupSwitchState(members, disabled) {
|
|
1905
1954
|
const list = members ?? [];
|
|
1906
1955
|
if (list.length === 0) return "empty";
|
|
@@ -8258,6 +8307,21 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
8258
8307
|
const names = body.runningAgents.map(String).filter((name) => name !== "");
|
|
8259
8308
|
return names.length === 0 ? void 0 : names;
|
|
8260
8309
|
}
|
|
8310
|
+
/**
|
|
8311
|
+
* Whether the "compatible with this host" filter starts on, and remembers the
|
|
8312
|
+
* answer once the user disagrees with the default (#872).
|
|
8313
|
+
*
|
|
8314
|
+
* Storage may be unavailable (private mode, a locked-down webview); the
|
|
8315
|
+
* default is what matters, so a failure to read it is not a failure.
|
|
8316
|
+
*/
|
|
8317
|
+
const HOST_FILTER_KEY = "dshm-host-filter";
|
|
8318
|
+
function readHostFilterPreference() {
|
|
8319
|
+
try {
|
|
8320
|
+
return localStorage.getItem(HOST_FILTER_KEY) !== "all";
|
|
8321
|
+
} catch {
|
|
8322
|
+
return true;
|
|
8323
|
+
}
|
|
8324
|
+
}
|
|
8261
8325
|
function MarketSection(props) {
|
|
8262
8326
|
const t = props.t;
|
|
8263
8327
|
const initialWebdav = (0, react.useMemo)(savedWebdav, []);
|
|
@@ -8811,7 +8875,16 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
8811
8875
|
/** Undefined while the first catalog response is pending; null means the host cannot be located. */
|
|
8812
8876
|
const [hostVersion, setHostVersion] = (0, react.useState)(void 0);
|
|
8813
8877
|
/** v1 is deliberately opt-in: undeclared/unknown entries stay visible even when enabled. */
|
|
8814
|
-
const [compatibleWithHost,
|
|
8878
|
+
const [compatibleWithHost, setCompatibleWithHostState] = (0, react.useState)(readHostFilterPreference);
|
|
8879
|
+
const setCompatibleWithHost = (0, react.useCallback)((next) => {
|
|
8880
|
+
setCompatibleWithHostState(next);
|
|
8881
|
+
try {
|
|
8882
|
+
localStorage.setItem(HOST_FILTER_KEY, next ? "compatible" : "all");
|
|
8883
|
+
} catch {}
|
|
8884
|
+
}, []);
|
|
8885
|
+
/** The profile this page is running in, for the manual command's own
|
|
8886
|
+
* `--profile` token (#872). The catalog's is always `web`. */
|
|
8887
|
+
const [profileName, setProfileName] = (0, react.useState)(null);
|
|
8815
8888
|
const [hostCompatibility, setHostCompatibility] = (0, react.useState)({});
|
|
8816
8889
|
const [hostCompatibilityPending, setHostCompatibilityPending] = (0, react.useState)(0);
|
|
8817
8890
|
/** Names already resolved or in flight; failed/unavailable names are released for an explicit retry. */
|
|
@@ -8880,6 +8953,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
8880
8953
|
}
|
|
8881
8954
|
if (body.notes !== null && typeof body.notes === "object" && !Array.isArray(body.notes)) setNotes(body.notes);
|
|
8882
8955
|
if (Array.isArray(body.patchDisabled)) setPatchDisabledNames(body.patchDisabled);
|
|
8956
|
+
if (typeof body.profile === "string") setProfileName(body.profile);
|
|
8883
8957
|
if (Array.isArray(body.unbundled)) setUnbundledNames(body.unbundled);
|
|
8884
8958
|
setConfigNamespaceIssues(Array.isArray(body.configNamespaces) ? body.configNamespaces.filter((item) => typeof item === "object" && item !== null && typeof item.name === "string" && Array.isArray(item.namespaces)) : []);
|
|
8885
8959
|
if (body.groups && typeof body.groups === "object") setGroups(body.groups);
|
|
@@ -9250,16 +9324,37 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
9250
9324
|
qInstalled,
|
|
9251
9325
|
installedView
|
|
9252
9326
|
]);
|
|
9253
|
-
const pluginsAll = (0, react.useMemo)(() =>
|
|
9254
|
-
|
|
9255
|
-
|
|
9256
|
-
|
|
9257
|
-
|
|
9258
|
-
|
|
9259
|
-
|
|
9260
|
-
|
|
9261
|
-
|
|
9262
|
-
|
|
9327
|
+
const { plugins: pluginsAll, hostHiddenCount } = (0, react.useMemo)(() => {
|
|
9328
|
+
if (data === null) return {
|
|
9329
|
+
plugins: [],
|
|
9330
|
+
hostHiddenCount: 0
|
|
9331
|
+
};
|
|
9332
|
+
const query = {
|
|
9333
|
+
category: cat,
|
|
9334
|
+
query: q,
|
|
9335
|
+
lang,
|
|
9336
|
+
categories: data.categories,
|
|
9337
|
+
sort: `${sortField}-${sortDir}`,
|
|
9338
|
+
sinceDays: timeRange === "all" ? void 0 : TIME_RANGE_DAYS[timeRange],
|
|
9339
|
+
hostCompatibility
|
|
9340
|
+
};
|
|
9341
|
+
const unfiltered = visiblePlugins(data.plugins, {
|
|
9342
|
+
...query,
|
|
9343
|
+
compatibleWithHost: false
|
|
9344
|
+
});
|
|
9345
|
+
if (!compatibleWithHost) return {
|
|
9346
|
+
plugins: unfiltered,
|
|
9347
|
+
hostHiddenCount: 0
|
|
9348
|
+
};
|
|
9349
|
+
const filtered = visiblePlugins(data.plugins, {
|
|
9350
|
+
...query,
|
|
9351
|
+
compatibleWithHost: true
|
|
9352
|
+
});
|
|
9353
|
+
return {
|
|
9354
|
+
plugins: filtered,
|
|
9355
|
+
hostHiddenCount: unfiltered.length - filtered.length
|
|
9356
|
+
};
|
|
9357
|
+
}, [
|
|
9263
9358
|
data,
|
|
9264
9359
|
q,
|
|
9265
9360
|
cat,
|
|
@@ -12871,7 +12966,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
12871
12966
|
})]
|
|
12872
12967
|
}), compatibleWithHost && typeof hostVersion === "string" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
12873
12968
|
className: Market_module_css_default.hostFilterNote,
|
|
12874
|
-
children: hostCompatibilityPending > 0 || loadedHostPackages < allHostPackages.length ? t("hostFilterLoading").replace("{0}", String(loadedHostPackages)).replace("{1}", String(allHostPackages.length)) : t("hostFilterActive").replace("{0}", hostVersion)
|
|
12969
|
+
children: hostCompatibilityPending > 0 || loadedHostPackages < allHostPackages.length ? t("hostFilterLoading").replace("{0}", String(loadedHostPackages)).replace("{1}", String(allHostPackages.length)) : t("hostFilterActive").replace("{0}", hostVersion).replace("{1}", String(hostHiddenCount))
|
|
12875
12970
|
})]
|
|
12876
12971
|
})]
|
|
12877
12972
|
}),
|
|
@@ -14124,17 +14219,28 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
14124
14219
|
title: t("cmdDetails"),
|
|
14125
14220
|
open: cmdOpen,
|
|
14126
14221
|
onToggle: () => setCmdOpen((o) => !o),
|
|
14127
|
-
children:
|
|
14128
|
-
|
|
14129
|
-
|
|
14130
|
-
className: Market_module_css_default.
|
|
14131
|
-
children:
|
|
14132
|
-
|
|
14133
|
-
|
|
14134
|
-
|
|
14135
|
-
|
|
14136
|
-
|
|
14137
|
-
|
|
14222
|
+
children: (() => {
|
|
14223
|
+
const shown = installCommandFor(confirming.install, profileName);
|
|
14224
|
+
if (shown.unusable) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
14225
|
+
className: Market_module_css_default.confirmCmd,
|
|
14226
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
14227
|
+
className: Market_module_css_default.cmd,
|
|
14228
|
+
children: t("cmdDesktopUnavailable")
|
|
14229
|
+
})
|
|
14230
|
+
});
|
|
14231
|
+
if (shown.command === null) return null;
|
|
14232
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
14233
|
+
className: Market_module_css_default.confirmCmd,
|
|
14234
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
14235
|
+
className: Market_module_css_default.cmd,
|
|
14236
|
+
children: shown.command
|
|
14237
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfirmCopyButton, {
|
|
14238
|
+
text: shown.command,
|
|
14239
|
+
label: t("cmdCopy"),
|
|
14240
|
+
copiedLabel: t("cmdCopied")
|
|
14241
|
+
})]
|
|
14242
|
+
});
|
|
14243
|
+
})()
|
|
14138
14244
|
})]
|
|
14139
14245
|
}),
|
|
14140
14246
|
(looksTerminal(confirming, lang) || confirming.deprecated === true) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
package/lib/check.js
CHANGED
|
@@ -313,6 +313,33 @@ function parseSemver(value) {
|
|
|
313
313
|
pre: m[4] === undefined ? [] : m[4].split('.'),
|
|
314
314
|
};
|
|
315
315
|
}
|
|
316
|
+
/**
|
|
317
|
+
* Parse a comparator's target the way node-semver does: a PARTIAL version in
|
|
318
|
+
* a range is zero-filled, so `<0.3` is `<0.3.0` and `>=2` is `>=2.0.0`.
|
|
319
|
+
*
|
|
320
|
+
* Without this, every prerelease floor written as a partial — the
|
|
321
|
+
* `@deepseek-ai/dsh-*` peers of a whole family of connector plugins say
|
|
322
|
+
* `>=0.1.5 <0.3`, `>=0.1.7-rc <2`, `>=0.2.0-rc.0 <0.3` — parsed as unknown,
|
|
323
|
+
* `satisfiesRange` answered null, and the gate stood aside: measured against
|
|
324
|
+
* the host's own evaluator over 699 live catalog manifests, those were the
|
|
325
|
+
* last 7 of 2796 (package, runtime) pairs where the host refuses a plugin
|
|
326
|
+
* and the market still promised a restart would apply it.
|
|
327
|
+
*
|
|
328
|
+
* A BARE partial (`1.2`) is a different thing in semver — an x-range that
|
|
329
|
+
* means `>=1.2.0 <1.3.0` — so it deliberately stays unknown here rather than
|
|
330
|
+
* being mistaken for the exact version `1.2.0`.
|
|
331
|
+
*/
|
|
332
|
+
function parseComparatorTarget(target, op) {
|
|
333
|
+
const direct = parseSemver(target);
|
|
334
|
+
if (direct !== null)
|
|
335
|
+
return direct;
|
|
336
|
+
if (op === '')
|
|
337
|
+
return null;
|
|
338
|
+
const parts = target.trim().split('.');
|
|
339
|
+
if (parts.length > 3 || parts.some(part => !/^\d+$/u.test(part)))
|
|
340
|
+
return null;
|
|
341
|
+
return parseSemver([...parts, ...Array(3 - parts.length).fill('0')].join('.'));
|
|
342
|
+
}
|
|
316
343
|
function comparePre(a, b) {
|
|
317
344
|
const len = Math.max(a.length, b.length);
|
|
318
345
|
for (let i = 0; i < len; i += 1) {
|
|
@@ -441,7 +468,7 @@ export function satisfiesRange(version, range, options = {}) {
|
|
|
441
468
|
const m = /^(\^|~|>=|<=|>|<)?(.*)$/.exec(p);
|
|
442
469
|
const op = m?.[1] ?? '';
|
|
443
470
|
const target = (m?.[2] ?? '').trim();
|
|
444
|
-
const tv =
|
|
471
|
+
const tv = parseComparatorTarget(target, op);
|
|
445
472
|
if (tv === null)
|
|
446
473
|
return null;
|
|
447
474
|
const major = tv.major;
|
|
@@ -488,7 +515,7 @@ export function satisfiesRange(version, range, options = {}) {
|
|
|
488
515
|
// prerelease admission gate. Otherwise an unknown protocol such as
|
|
489
516
|
// `workspace:^` or `catalog:default` is misreported as a definite false
|
|
490
517
|
// whenever the resolved version happens to carry a prerelease tag.
|
|
491
|
-
if (
|
|
518
|
+
if (parseComparatorTarget(target, m[1] ?? '') === null)
|
|
492
519
|
return null;
|
|
493
520
|
return { op: m[1] ?? '', target };
|
|
494
521
|
};
|
package/lib/verify.js
CHANGED
|
@@ -186,6 +186,19 @@ const isDshPeer = (peer) => peer === '@deepseek-ai/dsh' || peer.startsWith('@dee
|
|
|
186
186
|
export function hostPeerGate(activeProfileDir, name, facts) {
|
|
187
187
|
return peerGateAt(join(activeProfileDir, 'node_modules', name), name, facts);
|
|
188
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* The unmet requirement, if any, and the remedy side it points at.
|
|
191
|
+
*
|
|
192
|
+
* Which side of the range the runtime sits on decides the remedy: below the
|
|
193
|
+
* floor, upgrading the host genuinely helps; above the cap (#757's own
|
|
194
|
+
* shape) no upgrade can satisfy it, and suggesting one points the wrong way
|
|
195
|
+
* (#758).
|
|
196
|
+
*/
|
|
197
|
+
function unmetOf(peer, range, runtimeVersion) {
|
|
198
|
+
const floor = /[0-9]+\.[0-9]+\.[0-9]+[-+0-9A-Za-z.]*/.exec(range);
|
|
199
|
+
const hostTooOld = floor !== null && compareSemver(runtimeVersion, floor[0]) < 0;
|
|
200
|
+
return { peer, range, runtimeVersion, hostTooOld };
|
|
201
|
+
}
|
|
189
202
|
/** Same prediction for a patch plugin resolved outside the profile's flat node_modules. */
|
|
190
203
|
function peerGateAt(packageDir, name, facts) {
|
|
191
204
|
if (facts.runtimeVersion === null)
|
|
@@ -201,22 +214,33 @@ function peerGateAt(packageDir, name, facts) {
|
|
|
201
214
|
if (version !== '' && (facts.exemptions[`${name}@${version}`] ?? []).includes(facts.runtimeVersion)) {
|
|
202
215
|
return null;
|
|
203
216
|
}
|
|
217
|
+
// An own `engines.dsh` range decides ALONE and peerDependencies are then
|
|
218
|
+
// ignored — the host's rule, read from unmetRequirements in
|
|
219
|
+
// @deepseek-ai/dsh-app-boot, and the reason a gate that only read peers
|
|
220
|
+
// was too lenient on 62 of 2796 (package, runtime) pairs measured over the
|
|
221
|
+
// live catalog: `dsh-file-explorer-kit` (engines.dsh ^0.1.1) and
|
|
222
|
+
// `dsh-workspace-kit` (^0.1.2-rc.1) declare NO dsh peer at all, so the
|
|
223
|
+
// family that refuses them was invisible here and the card promised a
|
|
224
|
+
// restart the host would not honour. A non-string range throws inside the
|
|
225
|
+
// host's own evaluator; that is not a verdict this can predict, so the
|
|
226
|
+
// old answer stands (#757).
|
|
227
|
+
const engines = manifest.engines?.dsh;
|
|
228
|
+
if (engines !== undefined) {
|
|
229
|
+
if (typeof engines !== 'string')
|
|
230
|
+
return null;
|
|
231
|
+
const range = engines.trim();
|
|
232
|
+
// `workspace:` is substituted only for PEERS: the host counts it as an
|
|
233
|
+
// unmet range for engines, so an empty or invalid range stays unmet too.
|
|
234
|
+
const satisfied = range === '' ? false : satisfiesRange(facts.runtimeVersion, range, { includePrerelease: true });
|
|
235
|
+
return satisfied ? null : unmetOf('engines.dsh', range, facts.runtimeVersion);
|
|
236
|
+
}
|
|
204
237
|
for (const [peer, requirement] of Object.entries(manifest.peerDependencies ?? {})) {
|
|
205
238
|
if (!isDshPeer(peer) || typeof requirement !== 'string')
|
|
206
239
|
continue;
|
|
207
240
|
const range = requirement.trim();
|
|
208
241
|
const satisfied = range === '' ? false : satisfiesRange(facts.runtimeVersion, range, { includePrerelease: true });
|
|
209
|
-
if (satisfied === false)
|
|
210
|
-
|
|
211
|
-
// below the floor, upgrading the host genuinely helps; above the cap
|
|
212
|
-
// (#757's own shape) no upgrade can satisfy it, and suggesting one
|
|
213
|
-
// points the wrong way (#758).
|
|
214
|
-
const floor = /[0-9]+\.[0-9]+\.[0-9]+[-+0-9A-Za-z.]*/.exec(range);
|
|
215
|
-
let hostTooOld = false;
|
|
216
|
-
if (floor !== null)
|
|
217
|
-
hostTooOld = compareSemver(facts.runtimeVersion, floor[0]) < 0;
|
|
218
|
-
return { peer, range, runtimeVersion: facts.runtimeVersion, hostTooOld };
|
|
219
|
-
}
|
|
242
|
+
if (satisfied === false)
|
|
243
|
+
return unmetOf(peer, range, facts.runtimeVersion);
|
|
220
244
|
}
|
|
221
245
|
return null;
|
|
222
246
|
}
|
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.27",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|
package/src/check.ts
CHANGED
|
@@ -520,6 +520,31 @@ function parseSemver(value: string): Semver | null {
|
|
|
520
520
|
}
|
|
521
521
|
}
|
|
522
522
|
|
|
523
|
+
/**
|
|
524
|
+
* Parse a comparator's target the way node-semver does: a PARTIAL version in
|
|
525
|
+
* a range is zero-filled, so `<0.3` is `<0.3.0` and `>=2` is `>=2.0.0`.
|
|
526
|
+
*
|
|
527
|
+
* Without this, every prerelease floor written as a partial — the
|
|
528
|
+
* `@deepseek-ai/dsh-*` peers of a whole family of connector plugins say
|
|
529
|
+
* `>=0.1.5 <0.3`, `>=0.1.7-rc <2`, `>=0.2.0-rc.0 <0.3` — parsed as unknown,
|
|
530
|
+
* `satisfiesRange` answered null, and the gate stood aside: measured against
|
|
531
|
+
* the host's own evaluator over 699 live catalog manifests, those were the
|
|
532
|
+
* last 7 of 2796 (package, runtime) pairs where the host refuses a plugin
|
|
533
|
+
* and the market still promised a restart would apply it.
|
|
534
|
+
*
|
|
535
|
+
* A BARE partial (`1.2`) is a different thing in semver — an x-range that
|
|
536
|
+
* means `>=1.2.0 <1.3.0` — so it deliberately stays unknown here rather than
|
|
537
|
+
* being mistaken for the exact version `1.2.0`.
|
|
538
|
+
*/
|
|
539
|
+
function parseComparatorTarget(target: string, op: string): Semver | null {
|
|
540
|
+
const direct = parseSemver(target)
|
|
541
|
+
if (direct !== null) return direct
|
|
542
|
+
if (op === '') return null
|
|
543
|
+
const parts = target.trim().split('.')
|
|
544
|
+
if (parts.length > 3 || parts.some(part => !/^\d+$/u.test(part))) return null
|
|
545
|
+
return parseSemver([...parts, ...Array<string>(3 - parts.length).fill('0')].join('.'))
|
|
546
|
+
}
|
|
547
|
+
|
|
523
548
|
function comparePre(a: string[], b: string[]): number {
|
|
524
549
|
const len = Math.max(a.length, b.length)
|
|
525
550
|
for (let i = 0; i < len; i += 1) {
|
|
@@ -643,7 +668,7 @@ export function satisfiesRange(
|
|
|
643
668
|
const m = /^(\^|~|>=|<=|>|<)?(.*)$/.exec(p)
|
|
644
669
|
const op = m?.[1] ?? ''
|
|
645
670
|
const target = (m?.[2] ?? '').trim()
|
|
646
|
-
const tv =
|
|
671
|
+
const tv = parseComparatorTarget(target, op)
|
|
647
672
|
if (tv === null) return null
|
|
648
673
|
const major = tv.major
|
|
649
674
|
const minor = tv.minor
|
|
@@ -688,7 +713,7 @@ export function satisfiesRange(
|
|
|
688
713
|
// prerelease admission gate. Otherwise an unknown protocol such as
|
|
689
714
|
// `workspace:^` or `catalog:default` is misreported as a definite false
|
|
690
715
|
// whenever the resolved version happens to carry a prerelease tag.
|
|
691
|
-
if (
|
|
716
|
+
if (parseComparatorTarget(target, m[1] ?? '') === null) return null
|
|
692
717
|
return { op: m[1] ?? '', target }
|
|
693
718
|
}
|
|
694
719
|
|
|
@@ -51,6 +51,7 @@ import type { OperationRecord } from './operations.ts'
|
|
|
51
51
|
import { Diagnostics } from './Diagnostics.tsx'
|
|
52
52
|
import { exportMarketLog } from './self-check.ts'
|
|
53
53
|
import {
|
|
54
|
+
installCommandFor,
|
|
54
55
|
api, applyGithubRouting, avatarColor, catalogEntryForInstalled, entryForDep, failureText, githubRouteCandidates, groupSwitchState, installedForCatalog, isGenerationSpec, isInstalled, localizeBilingual, localizeBilingualList, looksTerminal, matchInstalledName, orderedCategories, pluginCategories,
|
|
55
56
|
formatCount, pageItems, pluginName, blockAliases, pluginScreenshotCandidates, pluginScreenshots, pluginsForFavorites, queuedRowApplies, rankThemeScreenshots, readSession, releaseNotesHttpsImage, rememberGithubRoute, resetScreenshotsCache, resolveCatalogRestore, safeScreenshots, sanitizeReleaseNotesBody, staleFavoriteUrls, themePlugins as themePluginsOf, themeSwatch, TIME_RANGE_DAYS, visiblePlugins, restartHintKey,
|
|
56
57
|
} from './market-data.ts'
|
|
@@ -1723,6 +1724,18 @@ function blockedSessions(body: { runningAgents?: unknown }): string[] | undefine
|
|
|
1723
1724
|
return names.length === 0 ? undefined : names
|
|
1724
1725
|
}
|
|
1725
1726
|
|
|
1727
|
+
/**
|
|
1728
|
+
* Whether the "compatible with this host" filter starts on, and remembers the
|
|
1729
|
+
* answer once the user disagrees with the default (#872).
|
|
1730
|
+
*
|
|
1731
|
+
* Storage may be unavailable (private mode, a locked-down webview); the
|
|
1732
|
+
* default is what matters, so a failure to read it is not a failure.
|
|
1733
|
+
*/
|
|
1734
|
+
const HOST_FILTER_KEY = 'dshm-host-filter'
|
|
1735
|
+
function readHostFilterPreference(): boolean {
|
|
1736
|
+
try { return localStorage.getItem(HOST_FILTER_KEY) !== 'all' } catch { return true }
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1726
1739
|
export function MarketSection(props: MarketSectionProps) {
|
|
1727
1740
|
const t = props.t
|
|
1728
1741
|
const initialWebdav = useMemo(savedWebdav, [])
|
|
@@ -2320,7 +2333,20 @@ export function MarketSection(props: MarketSectionProps) {
|
|
|
2320
2333
|
/** Undefined while the first catalog response is pending; null means the host cannot be located. */
|
|
2321
2334
|
const [hostVersion, setHostVersion] = useState<string | null | undefined>(undefined)
|
|
2322
2335
|
/** v1 is deliberately opt-in: undeclared/unknown entries stay visible even when enabled. */
|
|
2323
|
-
|
|
2336
|
+
// #872: ON by default. The report measured it on a live 0.2.0-rc.2 host —
|
|
2337
|
+
// 175 discovered plugins, 45 of them declaring that host — so the default
|
|
2338
|
+
// list was ~74% installable-in-name-only, with nothing at list level saying
|
|
2339
|
+
// so. That is the wrong default per this project's own rule: the default has
|
|
2340
|
+
// to be the one that is right without reading anything. Turning it off is
|
|
2341
|
+
// one click in the filter menu, and the choice sticks.
|
|
2342
|
+
const [compatibleWithHost, setCompatibleWithHostState] = useState(readHostFilterPreference)
|
|
2343
|
+
const setCompatibleWithHost = useCallback((next: boolean) => {
|
|
2344
|
+
setCompatibleWithHostState(next)
|
|
2345
|
+
try { localStorage.setItem(HOST_FILTER_KEY, next ? 'compatible' : 'all') } catch { /* storage unavailable — the choice just won't survive a reload */ }
|
|
2346
|
+
}, [])
|
|
2347
|
+
/** The profile this page is running in, for the manual command's own
|
|
2348
|
+
* `--profile` token (#872). The catalog's is always `web`. */
|
|
2349
|
+
const [profileName, setProfileName] = useState<string | null>(null)
|
|
2324
2350
|
const [hostCompatibility, setHostCompatibility] = useState<HostCompatibilityMap>({})
|
|
2325
2351
|
const [hostCompatibilityPending, setHostCompatibilityPending] = useState(0)
|
|
2326
2352
|
/** Names already resolved or in flight; failed/unavailable names are released for an explicit retry. */
|
|
@@ -2405,6 +2431,7 @@ export function MarketSection(props: MarketSectionProps) {
|
|
|
2405
2431
|
setNotes(body.notes as Record<string, string>)
|
|
2406
2432
|
}
|
|
2407
2433
|
if (Array.isArray(body.patchDisabled)) setPatchDisabledNames(body.patchDisabled)
|
|
2434
|
+
if (typeof body.profile === 'string') setProfileName(body.profile)
|
|
2408
2435
|
if (Array.isArray(body.unbundled)) setUnbundledNames(body.unbundled)
|
|
2409
2436
|
setConfigNamespaceIssues(Array.isArray(body.configNamespaces)
|
|
2410
2437
|
? body.configNamespaces.filter((item: unknown): item is { name: string; namespaces: string[]; ownEntryIds: string[] } =>
|
|
@@ -2830,14 +2857,23 @@ export function MarketSection(props: MarketSectionProps) {
|
|
|
2830
2857
|
setShowTop(false)
|
|
2831
2858
|
}, [tab, q, cat, sortField, sortDir, timeRange, compatibleWithHost, qThemes, themeSortField, themeSortDir, themeTimeRange, qFavorites, favSortField, favSortDir, favTimeRange, qInstalled, installedView])
|
|
2832
2859
|
|
|
2833
|
-
|
|
2834
|
-
|
|
2860
|
+
// The list, plus how many entries the host filter took out of it (#872).
|
|
2861
|
+
// Both come from one pass over the same query, so the count is exactly
|
|
2862
|
+
// "what this filter is hiding from what you were looking at" and the chip
|
|
2863
|
+
// can say it without a second trip through the catalog.
|
|
2864
|
+
const { plugins: pluginsAll, hostHiddenCount } = useMemo(() => {
|
|
2865
|
+
if (data === null) return { plugins: [] as RegistryPlugin[], hostHiddenCount: 0 }
|
|
2866
|
+
const query = {
|
|
2835
2867
|
category: cat, query: q, lang, categories: data.categories,
|
|
2836
|
-
sort: `${sortField}-${sortDir}`,
|
|
2868
|
+
sort: `${sortField}-${sortDir}` as `${typeof sortField}-${typeof sortDir}`,
|
|
2837
2869
|
sinceDays: timeRange === 'all' ? undefined : TIME_RANGE_DAYS[timeRange],
|
|
2838
2870
|
hostCompatibility,
|
|
2839
|
-
|
|
2840
|
-
})
|
|
2871
|
+
}
|
|
2872
|
+
const unfiltered = visiblePlugins(data.plugins, { ...query, compatibleWithHost: false })
|
|
2873
|
+
if (!compatibleWithHost) return { plugins: unfiltered, hostHiddenCount: 0 }
|
|
2874
|
+
const filtered = visiblePlugins(data.plugins, { ...query, compatibleWithHost: true })
|
|
2875
|
+
return { plugins: filtered, hostHiddenCount: unfiltered.length - filtered.length }
|
|
2876
|
+
},
|
|
2841
2877
|
[data, q, cat, lang, sortField, sortDir, timeRange, hostCompatibility, compatibleWithHost])
|
|
2842
2878
|
// One plugin can be stored under the installed name or the catalog name.
|
|
2843
2879
|
// Both have to hide the card and skip Update all (#657).
|
|
@@ -6234,7 +6270,9 @@ export function MarketSection(props: MarketSectionProps) {
|
|
|
6234
6270
|
? t('hostFilterLoading')
|
|
6235
6271
|
.replace('{0}', String(loadedHostPackages))
|
|
6236
6272
|
.replace('{1}', String(allHostPackages.length))
|
|
6237
|
-
: t('hostFilterActive')
|
|
6273
|
+
: t('hostFilterActive')
|
|
6274
|
+
.replace('{0}', hostVersion)
|
|
6275
|
+
.replace('{1}', String(hostHiddenCount))}
|
|
6238
6276
|
</div>
|
|
6239
6277
|
)}
|
|
6240
6278
|
</div>
|
|
@@ -7328,12 +7366,20 @@ export function MarketSection(props: MarketSectionProps) {
|
|
|
7328
7366
|
open={cmdOpen}
|
|
7329
7367
|
onToggle={() => setCmdOpen(o => !o)}
|
|
7330
7368
|
>
|
|
7331
|
-
|
|
7332
|
-
|
|
7333
|
-
|
|
7334
|
-
|
|
7335
|
-
)
|
|
7336
|
-
|
|
7369
|
+
{(() => {
|
|
7370
|
+
// #872: the catalog's command names the `web` profile, which is
|
|
7371
|
+
// a different profile from the one this page is running in —
|
|
7372
|
+
// and on a desktop profile the CLI refuses that name outright.
|
|
7373
|
+
const shown = installCommandFor(confirming.install, profileName)
|
|
7374
|
+
if (shown.unusable) return <div className={css.confirmCmd}><div className={css.cmd}>{t('cmdDesktopUnavailable')}</div></div>
|
|
7375
|
+
if (shown.command === null) return null
|
|
7376
|
+
return (
|
|
7377
|
+
<div className={css.confirmCmd}>
|
|
7378
|
+
<div className={css.cmd}>{shown.command}</div>
|
|
7379
|
+
<ConfirmCopyButton text={shown.command} label={t('cmdCopy')} copiedLabel={t('cmdCopied')} />
|
|
7380
|
+
</div>
|
|
7381
|
+
)
|
|
7382
|
+
})()}
|
|
7337
7383
|
</ConfirmFold>
|
|
7338
7384
|
</div>
|
|
7339
7385
|
{(looksTerminal(confirming, lang) || confirming.deprecated === true) && (
|
package/src/client/locales.ts
CHANGED
|
@@ -316,6 +316,7 @@ export const zh = {
|
|
|
316
316
|
confirmInstall: '确认安装',
|
|
317
317
|
cmdDetails: '安装命令',
|
|
318
318
|
cmdCopy: '复制安装命令',
|
|
319
|
+
cmdDesktopUnavailable: '桌面端的插件由应用自己安装(用上面的「安装」按钮);`dsh plugin` 不接受桌面端这个 profile。',
|
|
319
320
|
cmdCopied: '已复制',
|
|
320
321
|
catsMore: '更多分类',
|
|
321
322
|
catsLess: '收起',
|
|
@@ -329,7 +330,7 @@ export const zh = {
|
|
|
329
330
|
hostDetecting: '正在识别宿主版本…',
|
|
330
331
|
hostUnknown: '宿主版本未知',
|
|
331
332
|
hostFilterLoading: '正在检查宿主要求 {0}/{1};未声明或无法确认的插件仍会显示',
|
|
332
|
-
hostFilterActive: '
|
|
333
|
+
hostFilterActive: '已隐藏 {1} 个与 DSH {0} 不兼容的插件;未声明版本的仍在列表里',
|
|
333
334
|
hostRequirement: 'DSH {0}',
|
|
334
335
|
hostRequirementLoading: '正在读取宿主要求…',
|
|
335
336
|
hostRequirementUndeclared: '未声明宿主要求',
|
|
@@ -1065,6 +1066,7 @@ export const en: Record<MarketKey, string> = {
|
|
|
1065
1066
|
confirmInstall: 'Confirm install',
|
|
1066
1067
|
cmdDetails: 'Install command',
|
|
1067
1068
|
cmdCopy: 'Copy install command',
|
|
1069
|
+
cmdDesktopUnavailable: 'The desktop app installs plugins itself — use the Install button above; `dsh plugin` refuses this profile.',
|
|
1068
1070
|
cmdCopied: 'Copied',
|
|
1069
1071
|
catsMore: 'More categories',
|
|
1070
1072
|
catsLess: 'Fewer categories',
|
|
@@ -1078,7 +1080,7 @@ export const en: Record<MarketKey, string> = {
|
|
|
1078
1080
|
hostDetecting: 'Detecting host version…',
|
|
1079
1081
|
hostUnknown: 'Host version unknown',
|
|
1080
1082
|
hostFilterLoading: 'Checking host requirements {0}/{1}; undeclared or unavailable plugins remain visible',
|
|
1081
|
-
hostFilterActive: '
|
|
1083
|
+
hostFilterActive: 'Hid {1} plugins incompatible with DSH {0}; those declaring no version stay listed',
|
|
1082
1084
|
hostRequirement: 'DSH {0}',
|
|
1083
1085
|
hostRequirementLoading: 'Reading host requirement…',
|
|
1084
1086
|
hostRequirementUndeclared: 'Host requirement undeclared',
|
|
@@ -318,6 +318,42 @@ export interface ActivationInfo {
|
|
|
318
318
|
loadedByProfile?: true
|
|
319
319
|
}
|
|
320
320
|
|
|
321
|
+
/**
|
|
322
|
+
* The install command shown on a plugin's card, corrected for THIS profile
|
|
323
|
+
* (#872).
|
|
324
|
+
*
|
|
325
|
+
* awesome-dsh-plugin generates the string once, for the catalog — always
|
|
326
|
+
* `dsh plugin --profile web add <spec>` — which is right on a `web` profile
|
|
327
|
+
* and wrong on every other one. On a desktop profile it is more than wrong:
|
|
328
|
+
* the DSH CLI refuses a profile by that name, so the command as printed
|
|
329
|
+
* cannot install anything there at all. Two outcomes, and the caller renders
|
|
330
|
+
* them differently because the user's next step differs:
|
|
331
|
+
*
|
|
332
|
+
* - `command`: the text to show and copy, with the profile it names corrected;
|
|
333
|
+
* - `unusable`: show no command at all (the market's own button is the way),
|
|
334
|
+
* because handing over a command that cannot work is worse than handing
|
|
335
|
+
* over none.
|
|
336
|
+
*
|
|
337
|
+
* A command with no `--profile` token is passed through untouched: it is not
|
|
338
|
+
* this function's business to add one.
|
|
339
|
+
*/
|
|
340
|
+
export function installCommandFor(
|
|
341
|
+
command: unknown,
|
|
342
|
+
profile: string | null | undefined,
|
|
343
|
+
): { command: string | null; unusable: boolean } {
|
|
344
|
+
if (typeof command !== 'string' || command.trim() === '') return { command: null, unusable: false }
|
|
345
|
+
const named = /--profile(?:=|\s+)(\S+)/u.exec(command)
|
|
346
|
+
if (named === null) return { command, unusable: false }
|
|
347
|
+
const wanted = typeof profile === 'string' ? profile.trim() : ''
|
|
348
|
+
if (wanted === '') return { command, unusable: false }
|
|
349
|
+
// The CLI refuses the desktop profile by name — inside the running app it
|
|
350
|
+
// can never be used, whatever the install layout is (src/index.ts states
|
|
351
|
+
// the same rule where it chooses the plugin manager).
|
|
352
|
+
if (wanted.toLowerCase() === 'desktop') return { command: null, unusable: true }
|
|
353
|
+
if (named[1] === wanted) return { command, unusable: false }
|
|
354
|
+
return { command: command.replace(named[0], `--profile ${wanted}`), unusable: false }
|
|
355
|
+
}
|
|
356
|
+
|
|
321
357
|
/** The /dsh-market/installed payload (fields the market UI consumes). */
|
|
322
358
|
export interface InstalledPayload {
|
|
323
359
|
profile?: string
|
package/src/verify.ts
CHANGED
|
@@ -251,10 +251,28 @@ export function hostPeerGate(activeProfileDir: string, name: string, facts: Host
|
|
|
251
251
|
return peerGateAt(join(activeProfileDir, 'node_modules', name), name, facts)
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
+
/**
|
|
255
|
+
* The unmet requirement, if any, and the remedy side it points at.
|
|
256
|
+
*
|
|
257
|
+
* Which side of the range the runtime sits on decides the remedy: below the
|
|
258
|
+
* floor, upgrading the host genuinely helps; above the cap (#757's own
|
|
259
|
+
* shape) no upgrade can satisfy it, and suggesting one points the wrong way
|
|
260
|
+
* (#758).
|
|
261
|
+
*/
|
|
262
|
+
function unmetOf(peer: string, range: string, runtimeVersion: string): PeerGate {
|
|
263
|
+
const floor = /[0-9]+\.[0-9]+\.[0-9]+[-+0-9A-Za-z.]*/.exec(range)
|
|
264
|
+
const hostTooOld = floor !== null && compareSemver(runtimeVersion, floor[0]) < 0
|
|
265
|
+
return { peer, range, runtimeVersion, hostTooOld }
|
|
266
|
+
}
|
|
267
|
+
|
|
254
268
|
/** Same prediction for a patch plugin resolved outside the profile's flat node_modules. */
|
|
255
269
|
function peerGateAt(packageDir: string, name: string, facts: HostRuntimeFacts): PeerGate | null {
|
|
256
270
|
if (facts.runtimeVersion === null) return null
|
|
257
|
-
let manifest: {
|
|
271
|
+
let manifest: {
|
|
272
|
+
version?: unknown
|
|
273
|
+
engines?: Record<string, unknown>
|
|
274
|
+
peerDependencies?: Record<string, unknown>
|
|
275
|
+
}
|
|
258
276
|
try {
|
|
259
277
|
manifest = JSON.parse(
|
|
260
278
|
readFileSync(join(packageDir, 'package.json'), 'utf8'),
|
|
@@ -266,20 +284,30 @@ function peerGateAt(packageDir: string, name: string, facts: HostRuntimeFacts):
|
|
|
266
284
|
if (version !== '' && (facts.exemptions[`${name}@${version}`] ?? []).includes(facts.runtimeVersion)) {
|
|
267
285
|
return null
|
|
268
286
|
}
|
|
287
|
+
// An own `engines.dsh` range decides ALONE and peerDependencies are then
|
|
288
|
+
// ignored — the host's rule, read from unmetRequirements in
|
|
289
|
+
// @deepseek-ai/dsh-app-boot, and the reason a gate that only read peers
|
|
290
|
+
// was too lenient on 62 of 2796 (package, runtime) pairs measured over the
|
|
291
|
+
// live catalog: `dsh-file-explorer-kit` (engines.dsh ^0.1.1) and
|
|
292
|
+
// `dsh-workspace-kit` (^0.1.2-rc.1) declare NO dsh peer at all, so the
|
|
293
|
+
// family that refuses them was invisible here and the card promised a
|
|
294
|
+
// restart the host would not honour. A non-string range throws inside the
|
|
295
|
+
// host's own evaluator; that is not a verdict this can predict, so the
|
|
296
|
+
// old answer stands (#757).
|
|
297
|
+
const engines = manifest.engines?.dsh
|
|
298
|
+
if (engines !== undefined) {
|
|
299
|
+
if (typeof engines !== 'string') return null
|
|
300
|
+
const range = engines.trim()
|
|
301
|
+
// `workspace:` is substituted only for PEERS: the host counts it as an
|
|
302
|
+
// unmet range for engines, so an empty or invalid range stays unmet too.
|
|
303
|
+
const satisfied = range === '' ? false : satisfiesRange(facts.runtimeVersion, range, { includePrerelease: true })
|
|
304
|
+
return satisfied ? null : unmetOf('engines.dsh', range, facts.runtimeVersion)
|
|
305
|
+
}
|
|
269
306
|
for (const [peer, requirement] of Object.entries(manifest.peerDependencies ?? {})) {
|
|
270
307
|
if (!isDshPeer(peer) || typeof requirement !== 'string') continue
|
|
271
308
|
const range = requirement.trim()
|
|
272
309
|
const satisfied = range === '' ? false : satisfiesRange(facts.runtimeVersion, range, { includePrerelease: true })
|
|
273
|
-
if (satisfied === false)
|
|
274
|
-
// Which side of the range the runtime sits on decides the remedy:
|
|
275
|
-
// below the floor, upgrading the host genuinely helps; above the cap
|
|
276
|
-
// (#757's own shape) no upgrade can satisfy it, and suggesting one
|
|
277
|
-
// points the wrong way (#758).
|
|
278
|
-
const floor = /[0-9]+\.[0-9]+\.[0-9]+[-+0-9A-Za-z.]*/.exec(range)
|
|
279
|
-
let hostTooOld = false
|
|
280
|
-
if (floor !== null) hostTooOld = compareSemver(facts.runtimeVersion, floor[0]) < 0
|
|
281
|
-
return { peer, range, runtimeVersion: facts.runtimeVersion, hostTooOld }
|
|
282
|
-
}
|
|
310
|
+
if (satisfied === false) return unmetOf(peer, range, facts.runtimeVersion)
|
|
283
311
|
}
|
|
284
312
|
return null
|
|
285
313
|
}
|