dshmarket 1.66.43 → 1.66.45

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