dshmarket 1.66.26 → 1.66.28

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);
@@ -9125,6 +9199,10 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
9125
9199
  (0, react.useEffect)(() => {
9126
9200
  if (busyUrl === null && updatingName === null) {
9127
9201
  setHostBusy(false);
9202
+ statusRef.current = {
9203
+ ...statusRef.current,
9204
+ busy: false
9205
+ };
9128
9206
  setProgressLine(null);
9129
9207
  setProgressPhase(null);
9130
9208
  setProgressCurrent(null);
@@ -9250,16 +9328,37 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
9250
9328
  qInstalled,
9251
9329
  installedView
9252
9330
  ]);
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
- }), [
9331
+ const { plugins: pluginsAll, hostHiddenCount } = (0, react.useMemo)(() => {
9332
+ if (data === null) return {
9333
+ plugins: [],
9334
+ hostHiddenCount: 0
9335
+ };
9336
+ const query = {
9337
+ category: cat,
9338
+ query: q,
9339
+ lang,
9340
+ categories: data.categories,
9341
+ sort: `${sortField}-${sortDir}`,
9342
+ sinceDays: timeRange === "all" ? void 0 : TIME_RANGE_DAYS[timeRange],
9343
+ hostCompatibility
9344
+ };
9345
+ const unfiltered = visiblePlugins(data.plugins, {
9346
+ ...query,
9347
+ compatibleWithHost: false
9348
+ });
9349
+ if (!compatibleWithHost) return {
9350
+ plugins: unfiltered,
9351
+ hostHiddenCount: 0
9352
+ };
9353
+ const filtered = visiblePlugins(data.plugins, {
9354
+ ...query,
9355
+ compatibleWithHost: true
9356
+ });
9357
+ return {
9358
+ plugins: filtered,
9359
+ hostHiddenCount: unfiltered.length - filtered.length
9360
+ };
9361
+ }, [
9263
9362
  data,
9264
9363
  q,
9265
9364
  cat,
@@ -12871,7 +12970,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
12871
12970
  })]
12872
12971
  }), compatibleWithHost && typeof hostVersion === "string" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
12873
12972
  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)
12973
+ 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
12974
  })]
12876
12975
  })]
12877
12976
  }),
@@ -14124,17 +14223,28 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
14124
14223
  title: t("cmdDetails"),
14125
14224
  open: cmdOpen,
14126
14225
  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
- })
14226
+ children: (() => {
14227
+ const shown = installCommandFor(confirming.install, profileName);
14228
+ if (shown.unusable) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
14229
+ className: Market_module_css_default.confirmCmd,
14230
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
14231
+ className: Market_module_css_default.cmd,
14232
+ children: t("cmdDesktopUnavailable")
14233
+ })
14234
+ });
14235
+ if (shown.command === null) return null;
14236
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
14237
+ className: Market_module_css_default.confirmCmd,
14238
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
14239
+ className: Market_module_css_default.cmd,
14240
+ children: shown.command
14241
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfirmCopyButton, {
14242
+ text: shown.command,
14243
+ label: t("cmdCopy"),
14244
+ copiedLabel: t("cmdCopied")
14245
+ })]
14246
+ });
14247
+ })()
14138
14248
  })]
14139
14249
  }),
14140
14250
  (looksTerminal(confirming, lang) || confirming.deprecated === true) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
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.26",
4
+ "version": "1.66.28",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/types/index.d.ts",
@@ -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[] } =>
@@ -2700,6 +2727,12 @@ export function MarketSection(props: MarketSectionProps) {
2700
2727
  // that stale sample; the guarded restart route still handles the small
2701
2728
  // post-response lock-release window with its existing 409 retry.
2702
2729
  setHostBusy(false)
2730
+ // The same sample is what `runQueuedNow` reads to decide whether the
2731
+ // host is free (#845 review). Left in place it answered 「另一个安装、
2732
+ // 更新或卸载还在进行」 about an idle host, for the up to 2 s until the
2733
+ // drain's next tick — and the idle poll that used to correct it is
2734
+ // exactly what #845 removed, so with no queued row it never came back.
2735
+ statusRef.current = { ...statusRef.current, busy: false }
2703
2736
  setProgressLine(null)
2704
2737
  setProgressPhase(null)
2705
2738
  setProgressCurrent(null)
@@ -2830,14 +2863,23 @@ export function MarketSection(props: MarketSectionProps) {
2830
2863
  setShowTop(false)
2831
2864
  }, [tab, q, cat, sortField, sortDir, timeRange, compatibleWithHost, qThemes, themeSortField, themeSortDir, themeTimeRange, qFavorites, favSortField, favSortDir, favTimeRange, qInstalled, installedView])
2832
2865
 
2833
- const pluginsAll = useMemo(
2834
- () => (data === null ? [] : visiblePlugins(data.plugins, {
2866
+ // The list, plus how many entries the host filter took out of it (#872).
2867
+ // Both come from one pass over the same query, so the count is exactly
2868
+ // "what this filter is hiding from what you were looking at" and the chip
2869
+ // can say it without a second trip through the catalog.
2870
+ const { plugins: pluginsAll, hostHiddenCount } = useMemo(() => {
2871
+ if (data === null) return { plugins: [] as RegistryPlugin[], hostHiddenCount: 0 }
2872
+ const query = {
2835
2873
  category: cat, query: q, lang, categories: data.categories,
2836
- sort: `${sortField}-${sortDir}`,
2874
+ sort: `${sortField}-${sortDir}` as `${typeof sortField}-${typeof sortDir}`,
2837
2875
  sinceDays: timeRange === 'all' ? undefined : TIME_RANGE_DAYS[timeRange],
2838
2876
  hostCompatibility,
2839
- compatibleWithHost,
2840
- })),
2877
+ }
2878
+ const unfiltered = visiblePlugins(data.plugins, { ...query, compatibleWithHost: false })
2879
+ if (!compatibleWithHost) return { plugins: unfiltered, hostHiddenCount: 0 }
2880
+ const filtered = visiblePlugins(data.plugins, { ...query, compatibleWithHost: true })
2881
+ return { plugins: filtered, hostHiddenCount: unfiltered.length - filtered.length }
2882
+ },
2841
2883
  [data, q, cat, lang, sortField, sortDir, timeRange, hostCompatibility, compatibleWithHost])
2842
2884
  // One plugin can be stored under the installed name or the catalog name.
2843
2885
  // Both have to hide the card and skip Update all (#657).
@@ -6234,7 +6276,9 @@ export function MarketSection(props: MarketSectionProps) {
6234
6276
  ? t('hostFilterLoading')
6235
6277
  .replace('{0}', String(loadedHostPackages))
6236
6278
  .replace('{1}', String(allHostPackages.length))
6237
- : t('hostFilterActive').replace('{0}', hostVersion)}
6279
+ : t('hostFilterActive')
6280
+ .replace('{0}', hostVersion)
6281
+ .replace('{1}', String(hostHiddenCount))}
6238
6282
  </div>
6239
6283
  )}
6240
6284
  </div>
@@ -7328,12 +7372,20 @@ export function MarketSection(props: MarketSectionProps) {
7328
7372
  open={cmdOpen}
7329
7373
  onToggle={() => setCmdOpen(o => !o)}
7330
7374
  >
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>
7375
+ {(() => {
7376
+ // #872: the catalog's command names the `web` profile, which is
7377
+ // a different profile from the one this page is running in —
7378
+ // and on a desktop profile the CLI refuses that name outright.
7379
+ const shown = installCommandFor(confirming.install, profileName)
7380
+ if (shown.unusable) return <div className={css.confirmCmd}><div className={css.cmd}>{t('cmdDesktopUnavailable')}</div></div>
7381
+ if (shown.command === null) return null
7382
+ return (
7383
+ <div className={css.confirmCmd}>
7384
+ <div className={css.cmd}>{shown.command}</div>
7385
+ <ConfirmCopyButton text={shown.command} label={t('cmdCopy')} copiedLabel={t('cmdCopied')} />
7386
+ </div>
7387
+ )
7388
+ })()}
7337
7389
  </ConfirmFold>
7338
7390
  </div>
7339
7391
  {(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