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 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: "已按 DSH {0} 筛选;未声明或无法确认兼容性的插件仍会显示",
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: "Filtered for DSH {0}; undeclared or unconfirmed plugins remain visible",
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, setCompatibleWithHost] = (0, react.useState)(false);
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)(() => data === null ? [] : visiblePlugins(data.plugins, {
9254
- category: cat,
9255
- query: q,
9256
- lang,
9257
- categories: data.categories,
9258
- sort: `${sortField}-${sortDir}`,
9259
- sinceDays: timeRange === "all" ? void 0 : TIME_RANGE_DAYS[timeRange],
9260
- hostCompatibility,
9261
- compatibleWithHost
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: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
14128
- className: Market_module_css_default.confirmCmd,
14129
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
14130
- className: Market_module_css_default.cmd,
14131
- children: confirming.install
14132
- }), typeof confirming.install === "string" && confirming.install !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfirmCopyButton, {
14133
- text: confirming.install,
14134
- label: t("cmdCopy"),
14135
- copiedLabel: t("cmdCopied")
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 = parseSemver(target);
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 (parseSemver(target) === null)
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
- // Which side of the range the runtime sits on decides the remedy:
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.25",
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 = parseSemver(target)
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 (parseSemver(target) === null) return null
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
- const [compatibleWithHost, setCompatibleWithHost] = useState(false)
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
- const pluginsAll = useMemo(
2834
- () => (data === null ? [] : visiblePlugins(data.plugins, {
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
- compatibleWithHost,
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').replace('{0}', hostVersion)}
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
- <div className={css.confirmCmd}>
7332
- <div className={css.cmd}>{confirming.install}</div>
7333
- {typeof confirming.install === 'string' && confirming.install !== '' && (
7334
- <ConfirmCopyButton text={confirming.install} label={t('cmdCopy')} copiedLabel={t('cmdCopied')} />
7335
- )}
7336
- </div>
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) && (
@@ -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: '已按 DSH {0} 筛选;未声明或无法确认兼容性的插件仍会显示',
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: 'Filtered for DSH {0}; undeclared or unconfirmed plugins remain visible',
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: { version?: unknown; peerDependencies?: Record<string, unknown> }
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
  }