dshmarket 1.70.3 → 1.71.0

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
@@ -398,6 +398,8 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
398
398
  updateFail: "更新失败",
399
399
  upToDate: "已是最新",
400
400
  updateHeldTag: "新版等待中",
401
+ updateHeldTarget: "最新版 {0} 刚发布,还要约 {1} 小时。先更新到 {2}。",
402
+ updateHeldTargetUnknown: "最新版 {0} 的发布时间暂不确定,还需等待。先更新到 {2}。",
401
403
  updateHeldHint: "这个 profile 要求新版本发布满一段时间才允许安装,大约还要 {1} 小时。",
402
404
  updateHeldEstimatedHint: "新版本已经出现在 /latest,但镜像的完整信息还没同步,所以先不提供更新;大约 {1} 分钟后会重新检查。",
403
405
  newerPrereleaseAvailable: "预发布版 {0} 已发布",
@@ -1159,6 +1161,8 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
1159
1161
  updateFail: "Update failed",
1160
1162
  upToDate: "Up to date",
1161
1163
  updateHeldTag: "new version waiting",
1164
+ updateHeldTarget: "Latest {0} was just released; about {1} hours to wait. Update to {2} for now.",
1165
+ updateHeldTargetUnknown: "The release time of latest {0} is unknown, so it still needs to wait. Update to {2} for now.",
1162
1166
  updateHeldHint: "This profile waits a while before it installs a fresh release; about {1} hour(s) left.",
1163
1167
  updateHeldEstimatedHint: "The new version is listed, but the registry mirror has not caught up with its metadata, so the update is held; the market checks again in about {1} minute(s).",
1164
1168
  newerPrereleaseAvailable: "prerelease {0} published",
@@ -1948,6 +1952,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
1948
1952
  if (row.restore === true && world.updates[row.name]?.kind === "linked" && findCatalogEntryForLocal(world.plugins, row.name) !== null) return null;
1949
1953
  return "no-update";
1950
1954
  }
1955
+ if (row.kind === "update" && row.eligibleVersion !== void 0 && row.eligibleVersion !== world.updates[row.name]?.eligibleVersion) return "target-changed";
1951
1956
  return null;
1952
1957
  }
1953
1958
  function installedForCatalog(installed, bundles) {
@@ -4156,9 +4161,9 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
4156
4161
  * scripts awaiting approval, a held release. `detail` is kept for the same
4157
4162
  * reason: it is the row's own report.
4158
4163
  *
4159
- * Everything else is deliberately dropped. `updateOptions`, `blockedBy` and
4160
- * `conflicts` only matter while a row is queued or waiting on the user, and
4161
- * neither of those states is persisted here. `needsRefresh` is written so the
4164
+ * Failed updates awaiting build approval also keep their original update
4165
+ * options so approving after a reload cannot select a different target.
4166
+ * `blockedBy` and `conflicts` are deliberately dropped. `needsRefresh` is written so the
4162
4167
  * stored shape stays the record's own, but the restore drops it: only the page
4163
4168
  * that wrote it can still be waiting for that reload.
4164
4169
  */
@@ -4172,6 +4177,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
4172
4177
  ...record.reason === void 0 ? {} : { reason: record.reason },
4173
4178
  ...record.detail === void 0 ? {} : { detail: record.detail },
4174
4179
  ...record.blockedBuilds === void 0 ? {} : { blockedBuilds: [...record.blockedBuilds] },
4180
+ ...record.kind === "update" && record.state === "failed" && (record.blockedBuilds?.length ?? 0) > 0 && record.updateOptions !== void 0 ? { updateOptions: { ...record.updateOptions } } : {},
4175
4181
  ...record.needsRefresh === true ? { needsRefresh: true } : {},
4176
4182
  ...record.heldRelease === void 0 ? {} : { heldRelease: { ...record.heldRelease } }
4177
4183
  }));
@@ -4222,6 +4228,13 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
4222
4228
  const detail = asText(row.detail);
4223
4229
  const blockedBuilds = asStringList(row.blockedBuilds);
4224
4230
  const heldRelease = asHeldRelease(row.heldRelease);
4231
+ const options = row.updateOptions !== null && typeof row.updateOptions === "object" && !Array.isArray(row.updateOptions) ? row.updateOptions : void 0;
4232
+ const updateOptions = kind === "update" && state === "failed" && blockedBuilds !== void 0 && options !== void 0 ? {
4233
+ ...typeof options.force === "boolean" ? { force: options.force } : {},
4234
+ ...typeof options.restore === "boolean" ? { restore: options.restore } : {},
4235
+ ...asText(options.compatVersion) === void 0 ? {} : { compatVersion: asText(options.compatVersion) },
4236
+ ...asText(options.eligibleVersion) === void 0 ? {} : { eligibleVersion: asText(options.eligibleVersion) }
4237
+ } : void 0;
4225
4238
  return {
4226
4239
  id,
4227
4240
  kind,
@@ -4231,6 +4244,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
4231
4244
  ...reason === void 0 ? {} : { reason },
4232
4245
  ...detail === void 0 ? {} : { detail },
4233
4246
  ...blockedBuilds === void 0 ? {} : { blockedBuilds },
4247
+ ...updateOptions === void 0 ? {} : { updateOptions },
4234
4248
  ...heldRelease === void 0 ? {} : { heldRelease }
4235
4249
  };
4236
4250
  }
@@ -8987,18 +9001,20 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
8987
9001
  kind,
8988
9002
  name,
8989
9003
  restore: options.restore === true,
9004
+ ...typeof options.eligibleVersion === "string" ? { eligibleVersion: options.eligibleVersion } : {},
8990
9005
  ...url === void 0 ? {} : { url }
8991
9006
  }, {
8992
9007
  installed,
8993
9008
  updates,
8994
9009
  plugins: data.plugins
8995
9010
  });
8996
- if (verdict !== null) return stale(verdict === "gone" ? t("agentQueueStaleGone") : t("agentQueueStaleNoUpdate"));
9011
+ if (verdict !== null) return stale(verdict === "target-changed" ? localizeBilingual("更新目标已变化,已跳过。请重新选择更新。 / The update target changed, so this task was skipped. Choose the update again.", lang) : verdict === "gone" ? t("agentQueueStaleGone") : t("agentQueueStaleNoUpdate"));
8997
9012
  if (kind === "install" && url === void 0) return [];
8998
9013
  const updateOptions = kind === "update" ? {
8999
9014
  force: options.force === true,
9000
9015
  restore: options.restore === true,
9001
- ...typeof options.compatVersion === "string" ? { compatVersion: options.compatVersion } : {}
9016
+ ...typeof options.compatVersion === "string" ? { compatVersion: options.compatVersion } : {},
9017
+ ...typeof options.eligibleVersion === "string" ? { eligibleVersion: options.eligibleVersion } : {}
9002
9018
  } : void 0;
9003
9019
  return [{
9004
9020
  ok: true,
@@ -9045,6 +9061,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
9045
9061
  }, [
9046
9062
  data,
9047
9063
  t,
9064
+ lang,
9048
9065
  queueSnapshotReady,
9049
9066
  installed,
9050
9067
  updates
@@ -10683,7 +10700,14 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
10683
10700
  body: "{}"
10684
10701
  }).catch(() => {});
10685
10702
  }, []);
10686
- const doUpdate = (0, react.useCallback)((name, force = false, restore = false, compatVersion, confirmAgentsRunning = false) => {
10703
+ const doUpdate = (0, react.useCallback)((name, force = false, restore = false, compatVersion, confirmAgentsRunning = false, eligibleVersion) => {
10704
+ const expectedVersion = compatVersion === void 0 && !restore ? eligibleVersion ?? updates[name]?.eligibleVersion : void 0;
10705
+ const updateOptions = {
10706
+ force,
10707
+ restore,
10708
+ compatVersion,
10709
+ eligibleVersion: expectedVersion
10710
+ };
10687
10711
  setInstallError(null);
10688
10712
  setActivationWarnings([]);
10689
10713
  setStaleName((prev) => prev === name ? null : prev);
@@ -10698,11 +10722,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
10698
10722
  kind: "update",
10699
10723
  name,
10700
10724
  state: "running",
10701
- updateOptions: {
10702
- force,
10703
- restore,
10704
- compatVersion
10705
- }
10725
+ updateOptions
10706
10726
  }));
10707
10727
  return fetch(api("/dsh-market/update"), {
10708
10728
  method: "POST",
@@ -10712,6 +10732,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
10712
10732
  ...force ? { force: true } : {},
10713
10733
  ...restore ? { restore: true } : {},
10714
10734
  ...compatVersion !== void 0 ? { compatVersion } : {},
10735
+ ...expectedVersion !== void 0 ? { eligibleVersion: expectedVersion } : {},
10715
10736
  ...confirmAgentsRunning ? { confirmAgentsRunning: true } : {}
10716
10737
  })
10717
10738
  }).then((res) => res.json().then((body) => ({
@@ -10726,6 +10747,15 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
10726
10747
  if (body.partial === true) setInstallError(t("partialNote"));
10727
10748
  return;
10728
10749
  }
10750
+ if (status === 409 && body.staleTarget === true) {
10751
+ setRecords((list) => patch(list, updateRecordId, {
10752
+ state: "failed",
10753
+ reason: String(body.error ?? "Update target changed")
10754
+ }));
10755
+ setInstallError(localizeBilingual(String(body.error ?? "Update target changed"), lang));
10756
+ refreshInstalled(true);
10757
+ return;
10758
+ }
10729
10759
  if (status === 200 && body.ok && body.skipped === "current") {
10730
10760
  setRecords((list) => drop(list, updateRecordId));
10731
10761
  refreshInstalled(true);
@@ -10776,7 +10806,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
10776
10806
  if (Array.isArray(body.ignoredBuilds) && body.ignoredBuilds.length > 0) setBuildsSkipped({
10777
10807
  updateName: name,
10778
10808
  names: body.ignoredBuilds.map(String),
10779
- restore
10809
+ updateOptions
10780
10810
  });
10781
10811
  const orphans = Array.isArray(body.orphanBundles) ? body.orphanBundles.map(String) : [];
10782
10812
  const failure = failureText(body);
@@ -10788,7 +10818,8 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
10788
10818
  ].filter(Boolean).join("\n").trim().slice(0, 1200);
10789
10819
  setRecords((list) => patch(list, updateRecordId, {
10790
10820
  state: "failed",
10791
- reason: clipped
10821
+ reason: clipped,
10822
+ ...Array.isArray(body.ignoredBuilds) && body.ignoredBuilds.length > 0 ? { blockedBuilds: body.ignoredBuilds.map(String) } : {}
10792
10823
  }));
10793
10824
  setInstallError((restore ? t("restoreFail") : t("updateFail")) + ": " + name + " — " + localizeBilingual(clipped, lang));
10794
10825
  }
@@ -10797,7 +10828,8 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
10797
10828
  refreshInstalled,
10798
10829
  installed,
10799
10830
  t,
10800
- lang
10831
+ lang,
10832
+ updates
10801
10833
  ]);
10802
10834
  const doSourceMigration = (0, react.useCallback)((name, confirmAgentsRunning = false, renameTo) => {
10803
10835
  setInstallError(null);
@@ -11397,7 +11429,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
11397
11429
  if (task.kind === "install") {
11398
11430
  const plugin = dataRef.current?.plugins.find((candidate) => candidate.url === task.url);
11399
11431
  if (plugin !== void 0) doInstallRef.current?.(plugin);
11400
- } else if (task.kind === "update") doUpdateRef.current?.(task.name, task.updateOptions?.force, task.updateOptions?.restore, task.updateOptions?.compatVersion);
11432
+ } else if (task.kind === "update") doUpdateRef.current?.(task.name, task.updateOptions?.force, task.updateOptions?.restore, task.updateOptions?.compatVersion, false, task.updateOptions?.eligibleVersion);
11401
11433
  else doUninstallRef.current?.(task.name);
11402
11434
  } finally {
11403
11435
  drainingRef.current = false;
@@ -11437,7 +11469,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
11437
11469
  doInstall(plugin);
11438
11470
  } else if (record.kind === "update") {
11439
11471
  setRecords((prev) => drop(prev, record.id));
11440
- doUpdate(record.name, record.updateOptions?.force, record.updateOptions?.restore, record.updateOptions?.compatVersion, confirmAgentsRunning);
11472
+ doUpdate(record.name, record.updateOptions?.force, record.updateOptions?.restore, record.updateOptions?.compatVersion, confirmAgentsRunning, record.updateOptions?.eligibleVersion);
11441
11473
  } else {
11442
11474
  setRecords((prev) => drop(prev, record.id));
11443
11475
  doUninstall(record.name, confirmAgentsRunning);
@@ -11704,22 +11736,26 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
11704
11736
  });
11705
11737
  }, [bootId]);
11706
11738
  const doUpdateAll = (0, react.useCallback)(() => {
11707
- const names = reminderBatchUpdatableNames.slice();
11739
+ const names = reminderBatchUpdatableNames.map((name) => ({
11740
+ name,
11741
+ eligibleVersion: updates[name]?.eligibleVersion
11742
+ }));
11708
11743
  setUpdatingAll(true);
11709
11744
  const next = () => {
11710
- const name = names.shift();
11711
- if (name === void 0) {
11745
+ const item = names.shift();
11746
+ if (item === void 0) {
11712
11747
  setUpdatingAll(false);
11713
11748
  refreshInstalled(true);
11714
11749
  return;
11715
11750
  }
11716
- doUpdate(name).then(next, next);
11751
+ doUpdate(item.name, false, false, void 0, false, item.eligibleVersion).then(next, next);
11717
11752
  };
11718
11753
  next();
11719
11754
  }, [
11720
11755
  reminderBatchUpdatableNames,
11721
11756
  doUpdate,
11722
- refreshInstalled
11757
+ refreshInstalled,
11758
+ updates
11723
11759
  ]);
11724
11760
  const finishRestore = (0, react.useCallback)((body) => {
11725
11761
  const errors = Array.isArray(body.errors) ? body.errors : [];
@@ -13123,7 +13159,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
13123
13159
  const plugin = record.url === void 0 ? void 0 : data?.plugins.find((p) => p.url === record.url);
13124
13160
  approveAndRetry(names, () => {
13125
13161
  if (plugin !== void 0) doInstall(plugin);
13126
- else doUpdate(record.name, false, false);
13162
+ else doUpdate(record.name, record.updateOptions?.force, record.updateOptions?.restore, record.updateOptions?.compatVersion, false, record.updateOptions?.eligibleVersion);
13127
13163
  });
13128
13164
  }
13129
13165
  })
@@ -13374,11 +13410,11 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
13374
13410
  size: "sm",
13375
13411
  disabled: busyUrl !== null,
13376
13412
  onClick: () => {
13377
- const { plugin, updateName, names, restore } = buildsSkipped;
13413
+ const { plugin, updateName, names, updateOptions } = buildsSkipped;
13378
13414
  setBuildsSkipped(null);
13379
13415
  approveAndRetry(names, () => {
13380
13416
  if (plugin !== void 0) doInstall(plugin);
13381
- else if (updateName !== void 0) doUpdate(updateName, false, restore === true);
13417
+ else if (updateName !== void 0) doUpdate(updateName, updateOptions?.force, updateOptions?.restore, updateOptions?.compatVersion, false, updateOptions?.eligibleVersion);
13382
13418
  });
13383
13419
  },
13384
13420
  children: t("approveBuilds")
@@ -14594,56 +14630,70 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
14594
14630
  "aria-hidden": "true",
14595
14631
  children: "·"
14596
14632
  });
14597
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [updatable && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
14598
- className: Market_module_css_default.irowStatus,
14599
- children: [!generation && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
14600
- className: Market_module_css_default.irowStatusPart,
14601
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
14602
- className: Market_module_css_default.irowStatusFact,
14603
- children: status?.latest != null ? t("hostUpdateReady").replace("{0}", displayLatest(status.latest)) : t("updateAvailableShort")
14604
- }), sep]
14605
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
14606
- type: "button",
14607
- className: Market_module_css_default.notesLink,
14608
- onClick: () => openNotes(name, status?.current ?? null, status?.latest ?? null, repoUrl),
14609
- children: `${t("notesLink")} ›`
14610
- })]
14611
- }), (exempt || ignored) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
14612
- className: Market_module_css_default.irowStatus,
14613
- children: [
14614
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
14615
- className: Market_module_css_default.irowStatusIcon,
14616
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconBellOff, { size: 16 })
14617
- }),
14618
- exempt ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
14619
- className: Market_module_css_default.metaInline,
14620
- title: t("updateExemptHint"),
14621
- role: "status",
14622
- children: t("updateExemptMark")
14623
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
14624
- className: Market_module_css_default.metaInline,
14625
- role: "status",
14626
- children: t("updateNoticeIgnored")
14627
- }),
14628
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
14629
- className: Market_module_css_default.irowStatusAction,
14630
- children: exempt ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
14631
- type: "button",
14632
- className: Market_module_css_default.noteToggle,
14633
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
14634
+ updatable && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
14635
+ className: Market_module_css_default.irowStatus,
14636
+ children: [!generation && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
14637
+ className: Market_module_css_default.irowStatusPart,
14638
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
14639
+ className: Market_module_css_default.irowStatusFact,
14640
+ children: status?.eligibleVersion != null ? t("hostUpdateReady").replace("{0}", displayLatest(status.eligibleVersion)) : status?.latest != null ? t("hostUpdateReady").replace("{0}", displayLatest(status.latest)) : t("updateAvailableShort")
14641
+ }), sep]
14642
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
14643
+ type: "button",
14644
+ className: Market_module_css_default.notesLink,
14645
+ onClick: () => openNotes(name, status?.current ?? null, status?.latest ?? null, repoUrl),
14646
+ children: `${t("notesLink")} ›`
14647
+ })]
14648
+ }),
14649
+ updatable && status?.eligibleVersion !== void 0 && status.restoreRequired !== true && status.heldUpdate !== void 0 && status.heldUpdate.latest !== status.eligibleVersion && (() => {
14650
+ const left = Date.parse(status.heldUpdate.until) - Date.now();
14651
+ const known = status.heldUpdate.estimated !== true && Number.isFinite(left) && left > 0;
14652
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
14653
+ className: Market_module_css_default.irowStatus,
14654
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
14655
+ className: Market_module_css_default.metaInline,
14656
+ children: t(known ? "updateHeldTarget" : "updateHeldTargetUnknown").replace("{0}", displayLatest(status.heldUpdate.latest)).replace("{1}", String(Math.max(1, Math.ceil(left / 36e5)))).replace("{2}", displayLatest(status.eligibleVersion))
14657
+ })
14658
+ });
14659
+ })(),
14660
+ (exempt || ignored) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
14661
+ className: Market_module_css_default.irowStatus,
14662
+ children: [
14663
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
14664
+ className: Market_module_css_default.irowStatusIcon,
14665
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconBellOff, { size: 16 })
14666
+ }),
14667
+ exempt ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
14668
+ className: Market_module_css_default.metaInline,
14633
14669
  title: t("updateExemptHint"),
14634
- "aria-label": `${t("updateExemptRestore")} ${name}`,
14635
- onClick: () => toggleUpdateExempt(name),
14636
- children: t("updateExemptRestore")
14637
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
14638
- type: "button",
14639
- className: Market_module_css_default.noteToggle,
14640
- "aria-label": `${t("updateExemptRestore")} ${name}`,
14641
- onClick: () => unignoreUpdateNotice(name),
14642
- children: t("updateExemptRestore")
14670
+ role: "status",
14671
+ children: t("updateExemptMark")
14672
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
14673
+ className: Market_module_css_default.metaInline,
14674
+ role: "status",
14675
+ children: t("updateNoticeIgnored")
14676
+ }),
14677
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
14678
+ className: Market_module_css_default.irowStatusAction,
14679
+ children: exempt ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
14680
+ type: "button",
14681
+ className: Market_module_css_default.noteToggle,
14682
+ title: t("updateExemptHint"),
14683
+ "aria-label": `${t("updateExemptRestore")} ${name}`,
14684
+ onClick: () => toggleUpdateExempt(name),
14685
+ children: t("updateExemptRestore")
14686
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
14687
+ type: "button",
14688
+ className: Market_module_css_default.noteToggle,
14689
+ "aria-label": `${t("updateExemptRestore")} ${name}`,
14690
+ onClick: () => unignoreUpdateNotice(name),
14691
+ children: t("updateExemptRestore")
14692
+ })
14643
14693
  })
14644
- })
14645
- ]
14646
- })] });
14694
+ ]
14695
+ })
14696
+ ] });
14647
14697
  })(),
14648
14698
  !off && act !== void 0 && meta !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
14649
14699
  className: Market_module_css_default.act,
@@ -14783,7 +14833,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
14783
14833
  className: Market_module_css_default.metaTag,
14784
14834
  title: t("hostUpdateHint"),
14785
14835
  children: t("hostUpdateReady").replace("{0}", status.latest)
14786
- }) : status && status.updateAvailable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
14836
+ }) : status && status.updateAvailable ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
14787
14837
  variant: "outline",
14788
14838
  size: "sm",
14789
14839
  className: Market_module_css_default.warnBtn,
@@ -14792,8 +14842,11 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
14792
14842
  if (status.restoreRequired === true) askRestore(name);
14793
14843
  else doUpdate(name);
14794
14844
  },
14795
- children: status.restoreRequired === true ? t("restoreOnline") : t("update")
14796
- }) : status?.heldUpdate !== void 0 ? (() => {
14845
+ children: status.restoreRequired === true ? t("restoreOnline") : status.eligibleVersion !== void 0 ? `${t("update")} ${displayLatest(status.eligibleVersion)}` : t("update")
14846
+ }), status.heldUpdate !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
14847
+ className: Market_module_css_default.metaTag,
14848
+ children: t("updateHeldTag").replace("{0}", displayLatest(status.heldUpdate.latest))
14849
+ })] }) : status?.heldUpdate !== void 0 ? (() => {
14797
14850
  const left = Date.parse(status.heldUpdate.until) - Date.now();
14798
14851
  const estimated = status.heldUpdate.estimated === true;
14799
14852
  const amount = String(Math.max(1, Math.ceil(left / (estimated ? 6e4 : 36e5))));
package/lib/install.js CHANGED
@@ -119,6 +119,9 @@ export function isUnpublishedHostPeer(pkg, profile, explicitDir) {
119
119
  */
120
120
  export async function withHoistRecovery(run, profile, pluginArgs, profileDirectory, options = {}) {
121
121
  const marketFlags = options.marketFlags !== false;
122
+ // An already verified mature update must not rewrite the user's workspace
123
+ // policy as a side effect of market-level recovery (#991).
124
+ const repairProfilePolicy = options.preserveProfilePolicy !== true;
122
125
  /** The option a recovery step needed and this host does not accept (#732). */
123
126
  let unavailableOption = null;
124
127
  // Before the FIRST run, not after a failure: the shadowed duplicate rule
@@ -127,7 +130,7 @@ export async function withHoistRecovery(run, profile, pluginArgs, profileDirecto
127
130
  // small file read. Every verb that resolves the graph is covered, not just
128
131
  // add and remove: an `install` or an in-place `update` reads the same key.
129
132
  const verb = pluginArgs.find(argument => !argument.startsWith('-'));
130
- if (verb === 'add' || verb === 'remove' || verb === 'install' || verb === 'update') {
133
+ if (repairProfilePolicy && (verb === 'add' || verb === 'remove' || verb === 'install' || verb === 'update')) {
131
134
  const merged = mergeDuplicateReleaseAgeExcludes(profile, profileDirectory);
132
135
  if (merged.length > 0) {
133
136
  logEvent('warn', 'install', `minimumReleaseAgeExclude held several rules for ${merged.join(', ')}, and pnpm honours only the FIRST per name (#732) — merged each package's rules into one, before running`);
@@ -145,7 +148,7 @@ export async function withHoistRecovery(run, profile, pluginArgs, profileDirecto
145
148
  // appended stayed dead (@Mlte0907, 1.66.7). The merge is a no-op when
146
149
  // nothing is duplicated, so asking it costs one file read.
147
150
  let repairedDuringRun = false;
148
- if (!ok(result) && !result.cancelled && (pluginArgs[0] === 'add' || pluginArgs[0] === 'remove')) {
151
+ if (repairProfilePolicy && !ok(result) && !result.cancelled && (pluginArgs[0] === 'add' || pluginArgs[0] === 'remove')) {
149
152
  const mergedNow = mergeDuplicateReleaseAgeExcludes(profile, profileDirectory);
150
153
  if (mergedNow.length > 0) {
151
154
  repairedDuringRun = true;
@@ -155,7 +158,7 @@ export async function withHoistRecovery(run, profile, pluginArgs, profileDirecto
155
158
  }
156
159
  if (!ok(result) && !result.cancelled && !repairedDuringRun) {
157
160
  const failure = classifyPnpmFailure(`${result.stderr}\n${result.stdout}`, result.exitCode);
158
- if (failure?.code === 'unparseable-build-key') {
161
+ if (failure?.code === 'unparseable-build-key' && repairProfilePolicy) {
159
162
  // The profile's own allowBuilds block is what fails, so no retry of the
160
163
  // same command can pass until it is repaired (#698). Drop only the
161
164
  // source-form keys this pnpm cannot parse — bare names stay, and on
@@ -187,15 +190,14 @@ export async function withHoistRecovery(run, profile, pluginArgs, profileDirecto
187
190
  // (#732) or extending a version union (#733). Either way the same
188
191
  // rewrite fixes it, needs no option at all, and so also works on the
189
192
  // desktop bridge that refuses options. Retry the SAME argv afterwards.
190
- const mergedNow = mergeDuplicateReleaseAgeExcludes(profile, profileDirectory);
193
+ const mergedNow = repairProfilePolicy ? mergeDuplicateReleaseAgeExcludes(profile, profileDirectory) : [];
191
194
  if (mergedNow.length > 0) {
192
195
  logEvent('warn', 'install', `pnpm appended a second minimumReleaseAgeExclude rule for ${mergedNow.join(', ')} and honours only the first, shadowing its own entry (#732) — merged them into one and retrying once`);
193
196
  result = await run(profile, pluginArgs);
194
197
  }
195
- else if (options.releaseAgeBypass === false) {
196
- // The caller declined the bypass (#594), and the duplicates were not
197
- // what failed: this violation is the profile's own policy doing its
198
- // job, so the command stands as it is.
198
+ else if (options.releaseAgeBypass === false || options.preserveProfilePolicy === true) {
199
+ // Fresh installs (#594) and mature updates/recovery (#991) never
200
+ // bypass age enforcement. Preserve the native failure and policy.
199
201
  }
200
202
  else if (!marketFlags) {
201
203
  unavailableOption = RELEASE_AGE_OVERRIDE;
@@ -399,7 +401,7 @@ export async function withHoistRecovery(run, profile, pluginArgs, profileDirecto
399
401
  // after a recovery retry or an official Desktop manager install. Repair
400
402
  // the final file now, before another plugin operation reads it; a successful
401
403
  // command must never be retried just because its exclusions needed merging.
402
- if (ok(result) && (verb === 'add' || verb === 'remove' || verb === 'install' || verb === 'update')) {
404
+ if (repairProfilePolicy && ok(result) && (verb === 'add' || verb === 'remove' || verb === 'install' || verb === 'update')) {
403
405
  try {
404
406
  const merged = mergeDuplicateReleaseAgeExcludes(profile, profileDirectory);
405
407
  if (merged.length > 0) {
@@ -0,0 +1,101 @@
1
+ function semver(value) {
2
+ if (typeof value !== 'string')
3
+ return null;
4
+ const matched = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/.exec(value);
5
+ if (!matched)
6
+ return null;
7
+ const prerelease = matched[4]?.split('.') ?? [];
8
+ if (prerelease.some(part => /^\d+$/.test(part) && part.length > 1 && part.startsWith('0')))
9
+ return null;
10
+ return { core: [BigInt(matched[1]), BigInt(matched[2]), BigInt(matched[3])], prerelease };
11
+ }
12
+ /** Accept stable SemVer, including build metadata, at both API boundaries. */
13
+ export function isStableReleaseVersion(value) {
14
+ const parsed = semver(value);
15
+ return parsed !== null && parsed.prerelease.length === 0;
16
+ }
17
+ function compare(a, b) {
18
+ for (let i = 0; i < 3; i++) {
19
+ if (a.core[i] !== b.core[i])
20
+ return a.core[i] > b.core[i] ? 1 : -1;
21
+ }
22
+ if (a.prerelease.length === 0 || b.prerelease.length === 0)
23
+ return Math.sign(b.prerelease.length - a.prerelease.length);
24
+ for (let i = 0; i < Math.max(a.prerelease.length, b.prerelease.length); i++) {
25
+ const x = a.prerelease[i], y = b.prerelease[i];
26
+ if (x === undefined)
27
+ return -1;
28
+ if (y === undefined)
29
+ return 1;
30
+ if (x === y)
31
+ continue;
32
+ const xn = /^\d+$/.test(x), yn = /^\d+$/.test(y);
33
+ if (xn && yn)
34
+ return BigInt(x) > BigInt(y) ? 1 : -1;
35
+ if (xn !== yn)
36
+ return xn ? -1 : 1;
37
+ return x > y ? 1 : -1;
38
+ }
39
+ return 0;
40
+ }
41
+ export function strictPublishTime(value) {
42
+ if (typeof value !== 'string' || !/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d+)?Z$/.test(value))
43
+ return null;
44
+ const timestamp = Date.parse(value);
45
+ if (!Number.isFinite(timestamp))
46
+ return null;
47
+ return new Date(timestamp).toISOString().slice(0, 19) === value.slice(0, 19) ? timestamp : null;
48
+ }
49
+ export function selectMatureUpdate(packument, installed, now, policy) {
50
+ const latest = typeof packument?.['dist-tags']?.latest === 'string' ? packument['dist-tags'].latest : null;
51
+ const empty = (reason) => ({ candidate: null, latest, held: null, reason, nextMaturityAt: null, latestMaturityAt: null });
52
+ const top = semver(latest);
53
+ if (!top || top.prerelease.length || !packument.versions || !packument.time ||
54
+ typeof packument.versions !== 'object' || typeof packument.time !== 'object' ||
55
+ !Object.hasOwn(packument.versions, latest) ||
56
+ (policy.latestFromEndpoint != null && policy.latestFromEndpoint !== latest))
57
+ return empty('inconsistent');
58
+ const ceiling = policy.maxVersion === undefined ? top : semver(policy.maxVersion);
59
+ if (!ceiling || ceiling.prerelease.length ||
60
+ (policy.maxVersion !== undefined && !Object.hasOwn(packument.versions, policy.maxVersion)))
61
+ return empty('inconsistent');
62
+ const current = semver(installed);
63
+ if (!current || current.prerelease.length)
64
+ return empty('invalid-installed');
65
+ const ageMs = policy.minimumReleaseAgeMs;
66
+ if (!Number.isSafeInteger(ageMs) || ageMs < 0 || !Number.isFinite(now))
67
+ return empty('policy-unknown');
68
+ let candidate = null;
69
+ let candidateSemver = null;
70
+ let nextMaturityAt = null;
71
+ for (const version of Object.keys(packument.versions)) {
72
+ const parsed = semver(version);
73
+ if (!parsed || parsed.prerelease.length || compare(parsed, current) <= 0 || compare(parsed, top) > 0 || compare(parsed, ceiling) > 0)
74
+ continue;
75
+ const published = strictPublishTime(packument.time[version]);
76
+ const exempt = policy.exemptPackage === true || policy.exemptVersions?.has(version) === true;
77
+ // A declared exemption is not evidence for a fabricated or absent timestamp.
78
+ if (published === null || published > now || (!exempt && published > now - ageMs)) {
79
+ if (published !== null && published <= now && Number.isFinite(published + ageMs)) {
80
+ const at = published + ageMs;
81
+ if (nextMaturityAt === null || at < nextMaturityAt)
82
+ nextMaturityAt = at;
83
+ }
84
+ continue;
85
+ }
86
+ if (candidateSemver === null || compare(parsed, candidateSemver) > 0) {
87
+ candidate = version;
88
+ candidateSemver = parsed;
89
+ }
90
+ }
91
+ const latestPublished = strictPublishTime(packument.time[latest]);
92
+ const latestExempt = policy.exemptPackage === true || policy.exemptVersions?.has(latest) === true;
93
+ const latestMature = latestPublished !== null && latestPublished <= now &&
94
+ (latestExempt || latestPublished <= now - ageMs);
95
+ return {
96
+ candidate, latest, held: latestMature ? null : latest,
97
+ reason: candidate ? 'eligible' : latestMature ? 'current' : latestPublished === null ? 'unknown-age' : 'held',
98
+ nextMaturityAt,
99
+ latestMaturityAt: latestPublished === null || latestExempt ? null : latestPublished + ageMs,
100
+ };
101
+ }