@shellraining/dsh-plugin-updates 0.1.1 → 0.1.2

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.
Files changed (3) hide show
  1. package/README.md +5 -1
  2. package/lib/client.js +119 -35
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -21,7 +21,11 @@ dsh plugin --profile web add @shellraining/dsh-plugin-updates
21
21
 
22
22
  插件不改 profile 文件,所有改动都调用宿主插件管理器的接口:先卸载旧版本,再安装 `包名@版本`,用精确版本号保存,并保留原来的启用状态。版本兼容检查、构建脚本审批和锁文件回滚都由宿主处理。
23
23
 
24
- 卸载后安装失败时,插件会保持未安装状态,提示里会显示 pnpm 的输出,并提供「恢复」按钮,按原来的版本重新安装。发布不足 24 小时的版本会标为「新发布」,pnpm 开启 `minimumReleaseAge` 时,这类版本可能装不上。
24
+ 操作前会重新读取安装状态,已经是目标版本时跳过;精确版本声明与实际安装版本不一致时,会停止更新,提示先修复安装。卸载失败后不会继续安装,也不会断言安装状态完全没变。
25
+
26
+ 卸载后安装失败时,已安装列表下方会保留完整错误、日志路径和「恢复」按钮,按原来的版本重新安装;恢复失败后可以再次重试。只有确认卸载成功后才提供恢复,不会覆盖随后通过其他入口安装的版本。错误摘要优先显示错误行,不会把 pnpm 开头的成功检查信息当成失败原因。
27
+
28
+ 发布不足 24 小时的版本会标为「新发布」,pnpm 开启 `minimumReleaseAge` 时,这类版本可能装不上。如果出现 `ERR_PNPM_RESOLUTION_POLICY_VIOLATIONS_UNHANDLED`,说明 pnpm 的策略处理发生内部错误,不代表供应链校验通过就能继续安装;插件会停止操作并保留日志,不会关闭安全策略。
25
29
 
26
30
  本插件不能在页面里更新自己,因为卸载自己时界面也会一起卸载。有新版本时,它的详情页会给出对应的 `dsh plugin` 命令,退出 DSH 后在终端运行即可。
27
31
 
package/lib/client.js CHANGED
@@ -1289,13 +1289,19 @@ var ZH = {
1289
1289
  "self.body": "\u5728\u9875\u9762\u91CC\u5378\u8F7D\u81EA\u5DF1\u4F1A\u8BA9\u8FD9\u4E2A\u754C\u9762\u4E00\u8D77\u6D88\u5931\u3002\u8BF7\u5148\u5B8C\u5168\u9000\u51FA DSH\uFF0C\u5728\u7EC8\u7AEF\u8FD0\u884C\u4E0B\u9762\u7684\u547D\u4EE4\uFF0C\u518D\u91CD\u65B0\u6253\u5F00\uFF1A",
1290
1290
  "self.copy": "\u590D\u5236\u547D\u4EE4",
1291
1291
  "self.copied": "\u5DF2\u590D\u5236",
1292
+ "error.policy": "pnpm \u5904\u7406 minimumReleaseAge \u65F6\u53D1\u751F\u5185\u90E8\u9519\u8BEF\u3002\u8BF7\u5347\u7EA7\u5BBF\u4E3B\u9644\u5E26\u7684 pnpm\uFF0C\u6216\u7B49\u53D1\u5E03\u7B49\u5F85\u671F\u7ED3\u675F\u540E\u91CD\u8BD5\uFF1B\u5B8C\u6574\u9519\u8BEF\u89C1\u4E0B\u65B9\u65E5\u5FD7",
1293
+ "error.inconsistent": "\u5B89\u88C5\u58F0\u660E\u4E0E\u5B9E\u9645\u7248\u672C\u4E0D\u4E00\u81F4\uFF0C\u8BF7\u5148\u7528\u5BBF\u4E3B\u63D2\u4EF6\u7BA1\u7406\u5668\u6216 CLI \u4FEE\u590D\u5B89\u88C5\uFF0C\u518D\u91CD\u8BD5\u66F4\u65B0",
1294
+ "error.stateChanged": "\u63D2\u4EF6\u5B89\u88C5\u72B6\u6001\u5DF2\u53D8\u5316\uFF0C\u8BF7\u5237\u65B0\u63D2\u4EF6\u5217\u8868\u540E\u91CD\u8BD5",
1295
+ "op.checking": "\u6B63\u5728\u6838\u5BF9 {name} \u7684\u5B89\u88C5\u72B6\u6001\u2026",
1296
+ "op.checkFailed": "\u65E0\u6CD5\u786E\u8BA4 {name} \u7684\u5B89\u88C5\u72B6\u6001\uFF0C\u5DF2\u505C\u6B62\u64CD\u4F5C\uFF1A{reason}",
1297
+ "op.current": "{name} \u5DF2\u662F {to}\uFF0C\u65E0\u9700\u91CD\u590D\u5B89\u88C5",
1292
1298
  "op.removing": "\u6B63\u5728\u5378\u8F7D {name} {from}\u2026",
1293
1299
  "op.installing": "\u6B63\u5728\u5B89\u88C5 {name} {to}\u2026",
1294
1300
  "op.restoring": "\u6B63\u5728\u6062\u590D {name} {to}\u2026",
1295
1301
  "op.done": "{name} \u5DF2\u5207\u6362\u5230 {to}",
1296
1302
  "op.restart": "{name} \u5DF2\u5207\u6362\u5230 {to}\uFF0C\u91CD\u542F DSH \u540E\u751F\u6548",
1297
1303
  "op.failed": "{name} \u5B89\u88C5 {to} \u5931\u8D25\uFF1A{reason}",
1298
- "op.removeFailed": "{name} \u5378\u8F7D\u5931\u8D25\uFF0C\u6CA1\u6709\u505A\u4EFB\u4F55\u6539\u52A8\uFF1A{reason}",
1304
+ "op.removeFailed": "{name} \u5378\u8F7D\u672A\u5B8C\u6210\uFF0C\u5DF2\u505C\u6B62\u540E\u7EED\u5B89\u88C5\uFF1A{reason}",
1299
1305
  "op.restore": "\u6062\u590D {from}",
1300
1306
  "op.dismiss": "\u5173\u95ED",
1301
1307
  "op.log": "\u65E5\u5FD7\uFF1A{path}"
@@ -1325,13 +1331,19 @@ var EN = {
1325
1331
  "self.body": "Uninstalling it from the page would unload this page too. Quit DSH completely, run this command in a terminal, then start DSH again:",
1326
1332
  "self.copy": "Copy command",
1327
1333
  "self.copied": "Copied",
1334
+ "error.policy": "pnpm hit an internal error while handling minimumReleaseAge. Upgrade the pnpm bundled with the host, or retry after the release-age waiting period; see the full error below",
1335
+ "error.inconsistent": "The declared and installed versions differ. Repair the installation with the host plugin manager or CLI before retrying",
1336
+ "error.stateChanged": "The installed state has changed. Refresh the plugin list and retry",
1337
+ "op.checking": "Checking the installed version of {name}\u2026",
1338
+ "op.checkFailed": "Could not confirm the installed state of {name}; stopped: {reason}",
1339
+ "op.current": "{name} is already at {to}; no reinstall needed",
1328
1340
  "op.removing": "Uninstalling {name} {from}\u2026",
1329
1341
  "op.installing": "Installing {name} {to}\u2026",
1330
1342
  "op.restoring": "Restoring {name} {to}\u2026",
1331
1343
  "op.done": "{name} is now at {to}",
1332
1344
  "op.restart": "{name} is now at {to}; restart DSH to apply it",
1333
1345
  "op.failed": "Could not install {name} {to}: {reason}",
1334
- "op.removeFailed": "Could not uninstall {name}; nothing changed: {reason}",
1346
+ "op.removeFailed": "Uninstalling {name} did not finish; installation was not started: {reason}",
1335
1347
  "op.restore": "Restore {from}",
1336
1348
  "op.dismiss": "Dismiss",
1337
1349
  "op.log": "Log: {path}"
@@ -1354,7 +1366,7 @@ function updateTarget(candidate, lookup) {
1354
1366
  return best !== void 0 && (0, import_gt.default)(best.version, candidate.version) ? best.version : void 0;
1355
1367
  }
1356
1368
  function isBusy(operation) {
1357
- return operation?.phase === "removing" || operation?.phase === "installing";
1369
+ return operation?.phase === "checking" || operation?.phase === "removing" || operation?.phase === "installing";
1358
1370
  }
1359
1371
  function record(value) {
1360
1372
  return typeof value === "object" && value !== null ? value : {};
@@ -1362,6 +1374,14 @@ function record(value) {
1362
1374
  function errorText(error) {
1363
1375
  return error instanceof Error ? error.message : String(error);
1364
1376
  }
1377
+ function failureReason(result) {
1378
+ if (!result.ok) return result.error.message;
1379
+ const { error, packageResult, application } = result.value;
1380
+ const diagnostic = error?.diagnostic?.trim();
1381
+ const lines = (diagnostic || packageResult?.output || "").split("\n").map((line) => line.trim()).filter(Boolean);
1382
+ const failure = lines.find((line) => /\bERR[_!\s]|\berror\b|\bfatal\b/i.test(line));
1383
+ return failure ?? error?.code ?? diagnostic ?? application;
1384
+ }
1365
1385
  function npmInstalled(bundle) {
1366
1386
  const prefix = `${bundle.name}@`;
1367
1387
  return bundle.installed && !bundle.official && bundle.availability === "profile" && bundle.removable && bundle.readOnlyReason === void 0 && (0, import_valid.default)(bundle.version ?? "") !== null && bundle.source?.startsWith(prefix) === true && (0, import_valid2.default)(bundle.source.slice(prefix.length)) !== null;
@@ -1422,6 +1442,13 @@ var UpdatesStore = class {
1422
1442
  if (this.state.status !== "idle") void this.load();
1423
1443
  };
1424
1444
  async load() {
1445
+ try {
1446
+ await this.loadBundles();
1447
+ } catch {
1448
+ if (!this.disposed) this.patch({ status: "error" });
1449
+ }
1450
+ }
1451
+ async loadBundles() {
1425
1452
  const generation = ++this.generation;
1426
1453
  this.patch({ status: this.state.status === "ready" ? "ready" : "loading" });
1427
1454
  const pm = this.ctx.remote.pluginManager;
@@ -1495,14 +1522,14 @@ var UpdatesStore = class {
1495
1522
  /** Switch one plugin to `version`: uninstall it, then install that exact version with its enabled state. */
1496
1523
  install = async (name2, version) => {
1497
1524
  const candidate = this.state.candidates.find((item) => item.name === name2);
1498
- if (candidate === void 0 || name2 === SELF || isBusy(this.state.operation)) return false;
1525
+ if (this.disposed || candidate === void 0 || name2 === SELF || isBusy(this.state.operation) || (0, import_valid.default)(version) === null) return false;
1499
1526
  return this.run({ name: name2, from: candidate.version, to: version, enabled: candidate.enabled, restoring: false }, true);
1500
1527
  };
1501
1528
  /** Put back the version a failed switch removed; the plugin is no longer installed, so only install. */
1502
1529
  restore = async () => {
1503
1530
  const failed = this.state.operation;
1504
- if (failed?.phase !== "failed" || failed.restoring) return;
1505
- await this.run({ name: failed.name, from: failed.to, to: failed.from, enabled: failed.enabled, restoring: true }, false);
1531
+ if (this.disposed || failed?.phase !== "failed" || !failed.recoverable) return;
1532
+ await this.run({ name: failed.name, from: failed.from, to: failed.from, enabled: failed.enabled, restoring: true }, false);
1506
1533
  };
1507
1534
  /** The outcome toast finished; the banner in the tab keeps the result. */
1508
1535
  toastDone = () => {
@@ -1525,34 +1552,75 @@ var UpdatesStore = class {
1525
1552
  };
1526
1553
  async run(base, remove) {
1527
1554
  const pm = this.ctx.remote.pluginManager;
1528
- const fail = (phase, result) => {
1555
+ let failurePhase = "check-failed";
1556
+ let recoverable = false;
1557
+ const fail = (result) => {
1529
1558
  const value = result.ok ? result.value : void 0;
1530
- const output = value?.packageResult?.output.trim();
1559
+ const output = value?.packageResult?.output.trim() || value?.error?.diagnostic;
1531
1560
  const logPath = value?.packageResult?.logPath;
1532
- const reason = result.ok ? value?.error?.diagnostic?.split("\n")[0] ?? value?.error?.code ?? value?.application ?? "" : result.error.message;
1533
- this.patch({ operation: { ...base, phase, reason, toast: Date.now(), ...output ? { output } : {}, ...logPath ? { logPath } : {} } });
1561
+ this.patch({ operation: {
1562
+ ...base,
1563
+ phase: failurePhase,
1564
+ recoverable,
1565
+ reason: failureReason(result),
1566
+ toast: Date.now(),
1567
+ ...output ? { output } : {},
1568
+ ...logPath ? { logPath } : {}
1569
+ } });
1534
1570
  return false;
1535
1571
  };
1536
- if (remove) {
1537
- this.patch({ operation: { ...base, phase: "removing" } });
1538
- const removed = await pm.removeBundle(base.name);
1539
- const stopProfile = removed.ok && removed.value.error?.code === "stop-profile";
1540
- if (!stopProfile && (!removed.ok || removed.value.application === "failed" || removed.value.application === "cancelled")) {
1541
- return fail("remove-failed", removed);
1572
+ this.patch({ operation: { ...base, phase: "checking" } });
1573
+ try {
1574
+ const listed = await pm.listBundles();
1575
+ if (this.disposed) return false;
1576
+ if (!listed.ok) return fail(listed);
1577
+ const current = listed.value.find((bundle) => bundle.name === base.name && bundle.installed);
1578
+ if (current !== void 0 && (0, import_valid.default)(current.version ?? "") === (0, import_valid.default)(base.to)) {
1579
+ this.patch({ operation: { ...base, phase: "current", toast: Date.now() } });
1580
+ return true;
1542
1581
  }
1582
+ if (remove) {
1583
+ if (current === void 0 || !npmInstalled(current)) {
1584
+ return fail({ ok: true, value: { application: "failed", error: { code: "plugin-state-changed" } } });
1585
+ }
1586
+ const declared = (0, import_valid.default)(current.source?.slice(`${base.name}@`.length) ?? "");
1587
+ if (declared !== null && declared !== (0, import_valid.default)(current.version ?? "")) {
1588
+ return fail({ ok: true, value: { application: "failed", error: { code: "installation-inconsistent" } } });
1589
+ }
1590
+ base = { ...base, from: current.version ?? base.from, enabled: current.enabled };
1591
+ failurePhase = "remove-failed";
1592
+ this.patch({ operation: { ...base, phase: "removing" } });
1593
+ const removed = await pm.removeBundle(base.name);
1594
+ const stopProfile = removed.ok && removed.value.error?.code === "stop-profile";
1595
+ if (!stopProfile && (!removed.ok || removed.value.application === "failed" || removed.value.application === "cancelled")) {
1596
+ return fail(removed);
1597
+ }
1598
+ recoverable = !stopProfile;
1599
+ } else {
1600
+ if (current !== void 0) {
1601
+ return fail({ ok: true, value: { application: "failed", error: { code: "plugin-state-changed" } } });
1602
+ }
1603
+ recoverable = true;
1604
+ }
1605
+ if (this.disposed) return false;
1606
+ failurePhase = "failed";
1607
+ this.patch({ operation: { ...base, phase: "installing" } });
1608
+ const installed = await pm.installBundle(`${base.name}@${base.to}`, {
1609
+ enabled: base.enabled,
1610
+ saveExact: true,
1611
+ requestId: `${SELF}:${Date.now()}`
1612
+ });
1613
+ if (!installed.ok || installed.value.application === "failed" || installed.value.application === "cancelled") {
1614
+ return fail(installed);
1615
+ }
1616
+ const to = installed.value.version ?? base.to;
1617
+ this.patch({ operation: { ...base, to, toast: Date.now(), phase: installed.value.application === "restart-required" ? "restart" : "done" } });
1618
+ return true;
1619
+ } catch (error) {
1620
+ return fail({ ok: false, error: { message: errorText(error) } });
1621
+ } finally {
1622
+ if (!this.disposed) await this.load();
1543
1623
  }
1544
- this.patch({ operation: { ...base, phase: "installing" } });
1545
- const installed = await pm.installBundle(`${base.name}@${base.to}`, {
1546
- enabled: base.enabled,
1547
- saveExact: true,
1548
- requestId: `${SELF}:${Date.now()}`
1549
- });
1550
- if (!installed.ok || installed.value.application === "failed" || installed.value.application === "cancelled") {
1551
- return fail("failed", installed);
1552
- }
1553
- const to = installed.value.version ?? base.to;
1554
- this.patch({ operation: { ...base, to, toast: Date.now(), phase: installed.value.application === "restart-required" ? "restart" : "done" } });
1555
- return true;
1556
1624
  }
1557
1625
  };
1558
1626
 
@@ -1618,10 +1686,11 @@ function scan(document2) {
1618
1686
  const end = card.querySelector('[role="switch"]')?.parentElement;
1619
1687
  if (name2 && end) cards.set(name2, mountIn(end, "card", true));
1620
1688
  }
1621
- return { ...head === void 0 ? {} : { head }, cards };
1689
+ const status = mountIn(group, "status", false);
1690
+ return { ...head === void 0 ? {} : { head }, status, cards };
1622
1691
  }
1623
1692
  function same(a, b) {
1624
- if (a.head !== b.head || a.cards.size !== b.cards.size) return false;
1693
+ if (a.head !== b.head || a.status !== b.status || a.cards.size !== b.cards.size) return false;
1625
1694
  for (const [name2, node] of a.cards) if (b.cards.get(name2) !== node) return false;
1626
1695
  return true;
1627
1696
  }
@@ -1675,8 +1744,16 @@ function SelfNotice({ t, version }) {
1675
1744
  ] });
1676
1745
  }
1677
1746
  function operationText(t, operation) {
1678
- const params = { name: operation.name, from: operation.from, to: operation.to, reason: operation.reason ?? "" };
1747
+ const raw = operation.reason ?? "";
1748
+ const reason = raw.includes("ERR_PNPM_RESOLUTION_POLICY_VIOLATIONS_UNHANDLED") ? t("error.policy") : raw === "installation-inconsistent" ? t("error.inconsistent") : raw === "plugin-state-changed" ? t("error.stateChanged") : raw;
1749
+ const params = { name: operation.name, from: operation.from, to: operation.to, reason };
1679
1750
  switch (operation.phase) {
1751
+ case "checking":
1752
+ return t("op.checking", params);
1753
+ case "check-failed":
1754
+ return t("op.checkFailed", params);
1755
+ case "current":
1756
+ return t("op.current", params);
1680
1757
  case "removing":
1681
1758
  return t("op.removing", params);
1682
1759
  case "installing":
@@ -1694,8 +1771,8 @@ function operationText(t, operation) {
1694
1771
  function OperationToast({ t, useUpdates, actions }) {
1695
1772
  const operation = useUpdates((state) => state.operation);
1696
1773
  if (operation?.toast === void 0) return null;
1697
- const failed = operation.phase === "failed" || operation.phase === "remove-failed";
1698
- const restorable = operation.phase === "failed" && !operation.restoring;
1774
+ const failed = operation.phase === "failed" || operation.phase === "remove-failed" || operation.phase === "check-failed";
1775
+ const restorable = operation.phase === "failed" && operation.recoverable;
1699
1776
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1700
1777
  import_dsh_client_ui_primitives.Toast,
1701
1778
  {
@@ -1713,12 +1790,12 @@ function OperationToast({ t, useUpdates, actions }) {
1713
1790
  function OperationBanner({ t, operation, actions }) {
1714
1791
  const params = { from: operation.from };
1715
1792
  const busy = isBusy(operation);
1716
- const failed = operation.phase === "failed" || operation.phase === "remove-failed";
1793
+ const failed = operation.phase === "failed" || operation.phase === "remove-failed" || operation.phase === "check-failed";
1717
1794
  const text = operationText(t, operation);
1718
1795
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dpu-banner", role: failed ? "alert" : "status", ...failed ? { "data-tone": "error" } : {}, children: [
1719
1796
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dpu-banner-row", children: [
1720
1797
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: text }),
1721
- operation.phase === "failed" && !operation.restoring ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Button, { size: "sm", variant: "outline", onClick: () => {
1798
+ operation.phase === "failed" && operation.recoverable ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Button, { size: "sm", variant: "outline", onClick: () => {
1722
1799
  void actions.restore();
1723
1800
  }, children: t("op.restore", params) }) : null,
1724
1801
  busy ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Button, { size: "sm", variant: "ghost", onClick: actions.dismiss, children: t("op.dismiss") })
@@ -1826,6 +1903,13 @@ function ListControls({ t, useUpdates, actions }) {
1826
1903
  if (target !== void 0 && candidate.name !== SELF) targets.set(candidate.name, target);
1827
1904
  }
1828
1905
  const portals = [];
1906
+ if (anchors.status !== void 0 && operation !== void 0) {
1907
+ portals.push((0, import_react_dom.createPortal)(
1908
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(OperationBanner, { t, operation, actions }),
1909
+ anchors.status,
1910
+ "operation"
1911
+ ));
1912
+ }
1829
1913
  if (anchors.head !== void 0 && targets.size > 0) {
1830
1914
  portals.push((0, import_react_dom.createPortal)(
1831
1915
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Button, { size: "sm", variant: "outline", icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives2.IconDownloadOutlineRegular, { size: 13 }), disabled: busy, onClick: () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shellraining/dsh-plugin-updates",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "description": "在插件页检查 npm 来源插件的新版本,一键更新或安装指定版本",
6
6
  "main": "./lib/index.js",