@shellraining/dsh-plugin-updates 0.1.0 → 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 +9 -6
  2. package/lib/client.js +132 -171
  3. package/package.json +7 -7
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # dsh-plugin-updates
2
2
 
3
- 在插件页加一个「更新」标签,检查从 npm 安装的插件有没有新版本,可以逐个更新、全部更新,或者在插件详情页里挑一个版本安装。
3
+ 检查从 npm 安装的插件有没有新版本。插件页的「已安装插件」列表里可以逐个更新或全部更新,插件详情页里还能挑一个版本安装。
4
4
 
5
5
  ## 安装
6
6
 
@@ -11,22 +11,25 @@ dsh plugin --profile web add @shellraining/dsh-plugin-updates
11
11
  ## 使用
12
12
 
13
13
  - **已安装插件列表**:有新版本时,「已安装插件」标题旁出现「全部更新(N)」,对应插件卡片的开关左边出现「更新到 x.y.z」。点卡片其他位置照常进入详情页。
14
- - **更新标签**:打开侧栏的「插件」,切到「更新」,列出所有从 npm 安装的插件,以及当前版本、最新版本和检查状态。点插件名或「详情」会打开官方的插件详情页。
15
14
  - **插件详情页**:有新版本时,标题旁显示「可更新」标签,右上角多一个「更新到 x.y.z」按钮。页面底部有版本列表,每个版本都能单独安装,不兼容当前 DSH 的版本不能点。
16
15
 
17
16
  只检查依赖记录为 `包名@版本范围` 的插件。Git、本地目录、tarball 来源的插件和 DSH 官方插件都会跳过。
18
17
 
19
- 打开标签页或详情页时检查一次,结果缓存 10 分钟,点「检查更新」会重新查询。查询直接从浏览器读 npm registry,用的是 profile 中 pnpm 配置的源,查不到时再试插件管理器配置的备选源(默认是 npmmirror)。需要认证的私有源无法检查。
18
+ 打开插件页或详情页时检查一次,结果缓存 10 分钟,过期后再打开会重新查询。查询直接从浏览器读 npm registry,用的是 profile 中 pnpm 配置的源,查不到时再试插件管理器配置的备选源(默认是 npmmirror)。需要认证的私有源无法检查。
20
19
 
21
20
  ## 更新是怎么做的
22
21
 
23
22
  插件不改 profile 文件,所有改动都调用宿主插件管理器的接口:先卸载旧版本,再安装 `包名@版本`,用精确版本号保存,并保留原来的启用状态。版本兼容检查、构建脚本审批和锁文件回滚都由宿主处理。
24
23
 
25
- 卸载后安装失败时,插件会保持未安装状态,提示里会显示 pnpm 的输出,并提供「恢复」按钮,按原来的版本重新安装。发布不足 24 小时的版本会标为「新发布」,pnpm 开启 `minimumReleaseAge` 时,这类版本可能装不上。
24
+ 操作前会重新读取安装状态,已经是目标版本时跳过;精确版本声明与实际安装版本不一致时,会停止更新,提示先修复安装。卸载失败后不会继续安装,也不会断言安装状态完全没变。
26
25
 
27
- 本插件不能在页面里更新自己,因为卸载自己时界面也会一起卸载。需要更新时,列表会给出对应的 `dsh plugin` 命令,退出 DSH 后在终端运行即可。
26
+ 卸载后安装失败时,已安装列表下方会保留完整错误、日志路径和「恢复」按钮,按原来的版本重新安装;恢复失败后可以再次重试。只有确认卸载成功后才提供恢复,不会覆盖随后通过其他入口安装的版本。错误摘要优先显示错误行,不会把 pnpm 开头的成功检查信息当成失败原因。
28
27
 
29
- 官方列表没有给卡片开放扩展点,列表上的按钮靠 `data-plugin-group="bundles"`、`data-plugin-package` 和开关的 `role="switch"` 这几个语义属性定位,见 `src/client/anchors.ts`。DSH 改了这部分结构时,列表上的按钮会消失,「更新」标签和详情页不受影响。
28
+ 发布不足 24 小时的版本会标为「新发布」,pnpm 开启 `minimumReleaseAge` 时,这类版本可能装不上。如果出现 `ERR_PNPM_RESOLUTION_POLICY_VIOLATIONS_UNHANDLED`,说明 pnpm 的策略处理发生内部错误,不代表供应链校验通过就能继续安装;插件会停止操作并保留日志,不会关闭安全策略。
29
+
30
+ 本插件不能在页面里更新自己,因为卸载自己时界面也会一起卸载。有新版本时,它的详情页会给出对应的 `dsh plugin` 命令,退出 DSH 后在终端运行即可。
31
+
32
+ 官方列表没有给卡片开放扩展点,列表上的按钮靠 `data-plugin-group="bundles"`、`data-plugin-package` 和开关的 `role="switch"` 这几个语义属性定位,见 `src/client/anchors.ts`。DSH 改了这部分结构时,列表上的按钮会消失,详情页不受影响。
30
33
 
31
34
  ## 开发
32
35
 
package/lib/client.js CHANGED
@@ -1265,29 +1265,14 @@ module.exports = __toCommonJS(index_exports);
1265
1265
  // src/client/locale.ts
1266
1266
  var LOCALE_NS = "dshPluginUpdates";
1267
1267
  var ZH = {
1268
- "tab": "\u66F4\u65B0",
1269
- "summary.loading": "\u6B63\u5728\u68C0\u67E5\u66F4\u65B0\u2026",
1270
- "summary.some": "{count} \u4E2A\u63D2\u4EF6\u53EF\u66F4\u65B0",
1271
- "summary.none": "\u6240\u6709 npm \u63D2\u4EF6\u90FD\u5DF2\u662F\u6700\u65B0\u7248\u672C",
1272
- "summary.empty": "\u6CA1\u6709\u4ECE npm \u5B89\u88C5\u7684\u63D2\u4EF6",
1273
- "summary.skipped": "{count} \u4E2A\u63D2\u4EF6\u4E0D\u662F\u4ECE npm \u5B89\u88C5\u7684\uFF0C\u672A\u68C0\u67E5",
1274
- "refresh": "\u68C0\u67E5\u66F4\u65B0",
1275
- "updateAll": "\u5168\u90E8\u66F4\u65B0",
1276
1268
  "updateAllCount": "\u5168\u90E8\u66F4\u65B0\uFF08{count}\uFF09",
1277
1269
  "updateLabel": "\u628A {name} \u66F4\u65B0\u5230 {version}",
1278
1270
  "updating": "\u66F4\u65B0\u4E2D\u2026",
1279
- "update": "\u66F4\u65B0",
1280
1271
  "updateTo": "\u66F4\u65B0\u5230 {version}",
1281
1272
  "install": "\u5B89\u88C5",
1282
- "details": "\u8BE6\u60C5",
1283
- "loadFailed": "\u8BFB\u53D6\u63D2\u4EF6\u5217\u8868\u5931\u8D25\uFF1A{reason}",
1284
- "retry": "\u91CD\u8BD5",
1285
1273
  "status.update": "\u53EF\u66F4\u65B0 {version}",
1286
- "status.latest": "\u5DF2\u662F\u6700\u65B0",
1287
- "status.checking": "\u68C0\u67E5\u4E2D",
1288
- "status.failed": "\u67E5\u8BE2\u5931\u8D25",
1274
+ "versions.loading": "\u6B63\u5728\u8BFB\u53D6\u7248\u672C\u2026",
1289
1275
  "version.current": "\u5F53\u524D {version}",
1290
- "version.arrow": "{from} \u2192 {to}",
1291
1276
  "versions.title": "\u7248\u672C",
1292
1277
  "versions.registry": "\u6765\u81EA {registry}",
1293
1278
  "versions.showAll": "\u663E\u793A\u5168\u90E8 {count} \u4E2A\u7248\u672C",
@@ -1304,41 +1289,32 @@ var ZH = {
1304
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",
1305
1290
  "self.copy": "\u590D\u5236\u547D\u4EE4",
1306
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",
1307
1298
  "op.removing": "\u6B63\u5728\u5378\u8F7D {name} {from}\u2026",
1308
1299
  "op.installing": "\u6B63\u5728\u5B89\u88C5 {name} {to}\u2026",
1309
1300
  "op.restoring": "\u6B63\u5728\u6062\u590D {name} {to}\u2026",
1310
1301
  "op.done": "{name} \u5DF2\u5207\u6362\u5230 {to}",
1311
1302
  "op.restart": "{name} \u5DF2\u5207\u6362\u5230 {to}\uFF0C\u91CD\u542F DSH \u540E\u751F\u6548",
1312
1303
  "op.failed": "{name} \u5B89\u88C5 {to} \u5931\u8D25\uFF1A{reason}",
1313
- "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}",
1314
1305
  "op.restore": "\u6062\u590D {from}",
1315
1306
  "op.dismiss": "\u5173\u95ED",
1316
1307
  "op.log": "\u65E5\u5FD7\uFF1A{path}"
1317
1308
  };
1318
1309
  var EN = {
1319
- "tab": "Updates",
1320
- "summary.loading": "Checking for updates\u2026",
1321
- "summary.some": "{count} plugin(s) can be updated",
1322
- "summary.none": "All npm plugins are up to date",
1323
- "summary.empty": "No plugins were installed from npm",
1324
- "summary.skipped": "{count} plugin(s) not installed from npm were skipped",
1325
- "refresh": "Check for updates",
1326
- "updateAll": "Update all",
1327
1310
  "updateAllCount": "Update all ({count})",
1328
1311
  "updateLabel": "Update {name} to {version}",
1329
1312
  "updating": "Updating\u2026",
1330
- "update": "Update",
1331
1313
  "updateTo": "Update to {version}",
1332
1314
  "install": "Install",
1333
- "details": "Details",
1334
- "loadFailed": "Could not read the plugin list: {reason}",
1335
- "retry": "Retry",
1336
1315
  "status.update": "Update {version}",
1337
- "status.latest": "Up to date",
1338
- "status.checking": "Checking",
1339
- "status.failed": "Check failed",
1316
+ "versions.loading": "Reading versions\u2026",
1340
1317
  "version.current": "Current {version}",
1341
- "version.arrow": "{from} \u2192 {to}",
1342
1318
  "versions.title": "Versions",
1343
1319
  "versions.registry": "From {registry}",
1344
1320
  "versions.showAll": "Show all {count} versions",
@@ -1355,13 +1331,19 @@ var EN = {
1355
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:",
1356
1332
  "self.copy": "Copy command",
1357
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",
1358
1340
  "op.removing": "Uninstalling {name} {from}\u2026",
1359
1341
  "op.installing": "Installing {name} {to}\u2026",
1360
1342
  "op.restoring": "Restoring {name} {to}\u2026",
1361
1343
  "op.done": "{name} is now at {to}",
1362
1344
  "op.restart": "{name} is now at {to}; restart DSH to apply it",
1363
1345
  "op.failed": "Could not install {name} {to}: {reason}",
1364
- "op.removeFailed": "Could not uninstall {name}; nothing changed: {reason}",
1346
+ "op.removeFailed": "Uninstalling {name} did not finish; installation was not started: {reason}",
1365
1347
  "op.restore": "Restore {from}",
1366
1348
  "op.dismiss": "Dismiss",
1367
1349
  "op.log": "Log: {path}"
@@ -1384,7 +1366,7 @@ function updateTarget(candidate, lookup) {
1384
1366
  return best !== void 0 && (0, import_gt.default)(best.version, candidate.version) ? best.version : void 0;
1385
1367
  }
1386
1368
  function isBusy(operation) {
1387
- return operation?.phase === "removing" || operation?.phase === "installing";
1369
+ return operation?.phase === "checking" || operation?.phase === "removing" || operation?.phase === "installing";
1388
1370
  }
1389
1371
  function record(value) {
1390
1372
  return typeof value === "object" && value !== null ? value : {};
@@ -1392,6 +1374,14 @@ function record(value) {
1392
1374
  function errorText(error) {
1393
1375
  return error instanceof Error ? error.message : String(error);
1394
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
+ }
1395
1385
  function npmInstalled(bundle) {
1396
1386
  const prefix = `${bundle.name}@`;
1397
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;
@@ -1416,7 +1406,7 @@ var UpdatesStore = class {
1416
1406
  this.ctx = ctx;
1417
1407
  }
1418
1408
  ctx;
1419
- state = { status: "idle", candidates: [], skipped: 0, lookups: {} };
1409
+ state = { status: "idle", candidates: [], lookups: {} };
1420
1410
  listeners = /* @__PURE__ */ new Set();
1421
1411
  generation = 0;
1422
1412
  disposed = false;
@@ -1441,25 +1431,28 @@ var UpdatesStore = class {
1441
1431
  setLookup(name2, lookup) {
1442
1432
  this.patch({ lookups: { ...this.state.lookups, [name2]: lookup } });
1443
1433
  }
1444
- /** A page opened: read the list the first time, and again once a lookup is older than the cache. */
1434
+ /** A page opened: read the list the first time, after a failure, and once a lookup is older than the cache. */
1445
1435
  ensure = () => {
1446
1436
  const now = Date.now();
1447
1437
  const stale = Object.values(this.state.lookups).some((lookup) => lookup.status === "ready" && now - lookup.at > CACHE_MS);
1448
- if (this.state.status === "idle" || this.state.status === "ready" && stale) void this.load(false);
1449
- };
1450
- /** Re-read the list and every lookup. */
1451
- refresh = () => {
1452
- void this.load(true);
1438
+ if (this.state.status === "idle" || this.state.status === "error" || this.state.status === "ready" && stale) void this.load();
1453
1439
  };
1454
1440
  /** Re-read the list after a Host change; lookups stay cached. */
1455
1441
  bundlesChanged = () => {
1456
- if (this.state.status !== "idle") void this.load(false);
1442
+ if (this.state.status !== "idle") void this.load();
1457
1443
  };
1458
- async load(force) {
1444
+ async load() {
1445
+ try {
1446
+ await this.loadBundles();
1447
+ } catch {
1448
+ if (!this.disposed) this.patch({ status: "error" });
1449
+ }
1450
+ }
1451
+ async loadBundles() {
1459
1452
  const generation = ++this.generation;
1460
1453
  this.patch({ status: this.state.status === "ready" ? "ready" : "loading" });
1461
1454
  const pm = this.ctx.remote.pluginManager;
1462
- if (force || this.registries === void 0) {
1455
+ if (this.registries === void 0) {
1463
1456
  const [registries, exemptions] = await Promise.all([pm.registries(), pm.listVersionExemptions()]);
1464
1457
  if (registries.ok) {
1465
1458
  const first = registries.value.registry ?? registries.value.resolved ?? DEFAULT_REGISTRY;
@@ -1470,7 +1463,7 @@ var UpdatesStore = class {
1470
1463
  const listed = await pm.listBundles();
1471
1464
  if (generation !== this.generation) return;
1472
1465
  if (!listed.ok) {
1473
- this.patch({ status: "error", error: listed.error.message });
1466
+ this.patch({ status: "error" });
1474
1467
  return;
1475
1468
  }
1476
1469
  const resolve = (text) => text === void 0 ? void 0 : this.ctx.locale.resolveText(text);
@@ -1485,13 +1478,12 @@ var UpdatesStore = class {
1485
1478
  ...icon === void 0 ? {} : { icon }
1486
1479
  };
1487
1480
  }).sort((a, b) => a.title.localeCompare(b.title));
1488
- const others = listed.value.filter((bundle) => bundle.installed && !bundle.official && bundle.availability === "profile");
1489
- this.patch({ status: "ready", candidates, skipped: others.length - npm.length });
1481
+ this.patch({ status: "ready", candidates });
1490
1482
  const now = Date.now();
1491
1483
  for (const candidate of candidates) {
1492
1484
  const lookup = this.state.lookups[candidate.name];
1493
1485
  const stale = lookup === void 0 || lookup.status === "error" || lookup.status === "ready" && now - lookup.at > CACHE_MS;
1494
- if (force || stale) void this.lookup(candidate.name);
1486
+ if (stale) void this.lookup(candidate.name);
1495
1487
  }
1496
1488
  }
1497
1489
  async lookup(name2) {
@@ -1530,14 +1522,14 @@ var UpdatesStore = class {
1530
1522
  /** Switch one plugin to `version`: uninstall it, then install that exact version with its enabled state. */
1531
1523
  install = async (name2, version) => {
1532
1524
  const candidate = this.state.candidates.find((item) => item.name === name2);
1533
- 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;
1534
1526
  return this.run({ name: name2, from: candidate.version, to: version, enabled: candidate.enabled, restoring: false }, true);
1535
1527
  };
1536
1528
  /** Put back the version a failed switch removed; the plugin is no longer installed, so only install. */
1537
1529
  restore = async () => {
1538
1530
  const failed = this.state.operation;
1539
- if (failed?.phase !== "failed" || failed.restoring) return;
1540
- 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);
1541
1533
  };
1542
1534
  /** The outcome toast finished; the banner in the tab keeps the result. */
1543
1535
  toastDone = () => {
@@ -1560,57 +1552,86 @@ var UpdatesStore = class {
1560
1552
  };
1561
1553
  async run(base, remove) {
1562
1554
  const pm = this.ctx.remote.pluginManager;
1563
- const fail = (phase, result) => {
1555
+ let failurePhase = "check-failed";
1556
+ let recoverable = false;
1557
+ const fail = (result) => {
1564
1558
  const value = result.ok ? result.value : void 0;
1565
- const output = value?.packageResult?.output.trim();
1559
+ const output = value?.packageResult?.output.trim() || value?.error?.diagnostic;
1566
1560
  const logPath = value?.packageResult?.logPath;
1567
- const reason = result.ok ? value?.error?.diagnostic?.split("\n")[0] ?? value?.error?.code ?? value?.application ?? "" : result.error.message;
1568
- 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
+ } });
1569
1570
  return false;
1570
1571
  };
1571
- if (remove) {
1572
- this.patch({ operation: { ...base, phase: "removing" } });
1573
- const removed = await pm.removeBundle(base.name);
1574
- const stopProfile = removed.ok && removed.value.error?.code === "stop-profile";
1575
- if (!stopProfile && (!removed.ok || removed.value.application === "failed" || removed.value.application === "cancelled")) {
1576
- 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;
1577
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();
1578
1623
  }
1579
- this.patch({ operation: { ...base, phase: "installing" } });
1580
- const installed = await pm.installBundle(`${base.name}@${base.to}`, {
1581
- enabled: base.enabled,
1582
- saveExact: true,
1583
- requestId: `${SELF}:${Date.now()}`
1584
- });
1585
- if (!installed.ok || installed.value.application === "failed" || installed.value.application === "cancelled") {
1586
- return fail("failed", installed);
1587
- }
1588
- const to = installed.value.version ?? base.to;
1589
- this.patch({ operation: { ...base, to, toast: Date.now(), phase: installed.value.application === "restart-required" ? "restart" : "done" } });
1590
- return true;
1591
1624
  }
1592
1625
  };
1593
1626
 
1594
1627
  // src/client/styles.ts
1595
1628
  var STYLE_ID = "@shellraining/dsh-plugin-updates";
1596
1629
  var CSS = `
1597
- .dpu-tab, .dpu-section { display: flex; flex-direction: column; gap: 12px; min-width: 0; color: var(--dsw-alias-label-primary); }
1630
+ .dpu-section { display: flex; flex-direction: column; gap: 12px; min-width: 0; color: var(--dsw-alias-label-primary); }
1598
1631
  .dpu-head { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; min-height: 28px; }
1599
1632
  .dpu-summary { flex: 1; min-width: 0; color: var(--dsw-alias-label-secondary); font-size: 13px; line-height: 20px; }
1600
1633
  .dpu-caption { color: var(--dsw-alias-label-caption); font-size: 12px; line-height: 18px; }
1601
1634
  .dpu-title { margin: 0; font-size: 14px; font-weight: 500; line-height: 20px; }
1602
- .dpu-list { display: flex; flex-direction: column; margin: 0; padding: 0; list-style: none; }
1603
- .dpu-card { display: flex; align-items: center; gap: 14px; min-width: 0; margin: 0 -8px; padding: 8px; border-radius: var(--dsw-radius-xl); }
1604
- .dpu-card:hover { background: var(--dsw-alias-interactive-bg-hover); }
1605
- .dpu-icon { flex: none; display: inline-flex; align-items: center; justify-content: center; width: 40px; height: 40px; overflow: hidden; border: .5px solid var(--dsw-alias-border-l3); border-radius: var(--dsw-radius-lg); color: var(--dsw-alias-label-secondary); }
1606
- .dpu-icon img { width: 100%; height: 100%; object-fit: cover; }
1607
- .dpu-main { flex: 1; display: flex; flex-direction: column; gap: 2px; min-width: 0; }
1608
- .dpu-name-row { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; min-width: 0; }
1609
- .dpu-name { min-width: 0; padding: 0; overflow: hidden; border: 0; background: none; color: inherit; font: inherit; font-size: 14px; font-weight: 500; line-height: 20px; text-align: left; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; }
1610
- .dpu-name:hover { text-decoration: underline; }
1611
- .dpu-name:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 2px; }
1612
- .dpu-meta { color: var(--dsw-alias-label-tertiary); font-size: 13px; line-height: 18px; font-variant-numeric: tabular-nums; }
1613
- .dpu-end { flex: none; display: inline-flex; align-items: center; gap: 8px; }
1614
1635
  .dpu-mono { font-family: var(--ds-font-family-code); font-variant-numeric: tabular-nums; }
1615
1636
  .dpu-rows { display: flex; flex-direction: column; margin: 0; padding: 0; list-style: none; border-top: .5px solid var(--dsw-alias-border-l2); }
1616
1637
  .dpu-row { display: flex; align-items: center; gap: 8px; min-height: 36px; padding: 2px 0; border-bottom: .5px solid var(--dsw-alias-border-l2); }
@@ -1665,10 +1686,11 @@ function scan(document2) {
1665
1686
  const end = card.querySelector('[role="switch"]')?.parentElement;
1666
1687
  if (name2 && end) cards.set(name2, mountIn(end, "card", true));
1667
1688
  }
1668
- return { ...head === void 0 ? {} : { head }, cards };
1689
+ const status = mountIn(group, "status", false);
1690
+ return { ...head === void 0 ? {} : { head }, status, cards };
1669
1691
  }
1670
1692
  function same(a, b) {
1671
- 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;
1672
1694
  for (const [name2, node] of a.cards) if (b.cards.get(name2) !== node) return false;
1673
1695
  return true;
1674
1696
  }
@@ -1707,16 +1729,6 @@ function selfCommand(version) {
1707
1729
  function dateText(stamp) {
1708
1730
  return stamp === void 0 ? "" : new Date(stamp).toLocaleDateString();
1709
1731
  }
1710
- function Artwork({ src }) {
1711
- const [broken, setBroken] = (0, import_react.useState)(false);
1712
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dpu-icon", "aria-hidden": "true", children: src === void 0 || broken ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.PluginArtworkDefault, { size: 28 }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src, alt: "", onError: () => setBroken(true) }) });
1713
- }
1714
- function StatusTag({ t, candidate, lookup }) {
1715
- if (lookup === void 0 || lookup.status === "loading") return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Tag, { tone: "quiet", children: t("status.checking") });
1716
- if (lookup.status === "error") return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Tag, { tone: "warning", children: t("status.failed") });
1717
- const target = updateTarget(candidate, lookup);
1718
- return target === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Tag, { tone: "quiet", children: t("status.latest") }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Tag, { tone: "info", children: t("status.update", { version: target }) });
1719
- }
1720
1732
  function SelfNotice({ t, version }) {
1721
1733
  const [copied, setCopied] = (0, import_react.useState)(false);
1722
1734
  const command = selfCommand(version);
@@ -1732,8 +1744,16 @@ function SelfNotice({ t, version }) {
1732
1744
  ] });
1733
1745
  }
1734
1746
  function operationText(t, operation) {
1735
- 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 };
1736
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);
1737
1757
  case "removing":
1738
1758
  return t("op.removing", params);
1739
1759
  case "installing":
@@ -1751,8 +1771,8 @@ function operationText(t, operation) {
1751
1771
  function OperationToast({ t, useUpdates, actions }) {
1752
1772
  const operation = useUpdates((state) => state.operation);
1753
1773
  if (operation?.toast === void 0) return null;
1754
- const failed = operation.phase === "failed" || operation.phase === "remove-failed";
1755
- 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;
1756
1776
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1757
1777
  import_dsh_client_ui_primitives.Toast,
1758
1778
  {
@@ -1770,12 +1790,12 @@ function OperationToast({ t, useUpdates, actions }) {
1770
1790
  function OperationBanner({ t, operation, actions }) {
1771
1791
  const params = { from: operation.from };
1772
1792
  const busy = isBusy(operation);
1773
- const failed = operation.phase === "failed" || operation.phase === "remove-failed";
1793
+ const failed = operation.phase === "failed" || operation.phase === "remove-failed" || operation.phase === "check-failed";
1774
1794
  const text = operationText(t, operation);
1775
1795
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dpu-banner", role: failed ? "alert" : "status", ...failed ? { "data-tone": "error" } : {}, children: [
1776
1796
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dpu-banner-row", children: [
1777
1797
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: text }),
1778
- 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: () => {
1779
1799
  void actions.restore();
1780
1800
  }, children: t("op.restore", params) }) : null,
1781
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") })
@@ -1784,63 +1804,6 @@ function OperationBanner({ t, operation, actions }) {
1784
1804
  failed && operation.logPath !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dpu-caption", children: t("op.log", { path: operation.logPath }) }) : null
1785
1805
  ] });
1786
1806
  }
1787
- function UpdatesTab({ t, useUpdates, actions, openPackage }) {
1788
- const state = useUpdates((snapshot) => snapshot);
1789
- (0, import_react.useEffect)(() => {
1790
- actions.ensure();
1791
- }, [actions]);
1792
- const busy = isBusy(state.operation);
1793
- const lookups = Object.values(state.lookups);
1794
- const checking = state.status === "loading" || lookups.some((lookup) => lookup.status === "loading");
1795
- const outdated = state.candidates.filter((item) => updateTarget(item, state.lookups[item.name]) !== void 0);
1796
- const pending = outdated.filter((item) => item.name !== SELF);
1797
- const self = outdated.find((item) => item.name === SELF);
1798
- const selfTarget = self === void 0 ? void 0 : updateTarget(self, state.lookups[SELF]);
1799
- let summary;
1800
- if (state.status === "idle" || state.status === "loading" && state.candidates.length === 0) summary = t("summary.loading");
1801
- else if (state.candidates.length === 0) summary = t("summary.empty");
1802
- else if (outdated.length > 0) summary = t("summary.some", { count: outdated.length });
1803
- else summary = checking ? t("summary.loading") : t("summary.none");
1804
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: "dpu-tab", "aria-busy": checking, children: [
1805
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dpu-head", children: [
1806
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dpu-summary", role: "status", children: summary }),
1807
- /* @__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.IconRefreshOutlineRegular, { size: 13 }), disabled: checking || busy, onClick: actions.refresh, children: t("refresh") }),
1808
- pending.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Button, { size: "sm", variant: "primary", disabled: busy, onClick: () => {
1809
- void actions.updateAll();
1810
- }, children: t("updateAll") }) : null
1811
- ] }),
1812
- state.operation === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(OperationBanner, { t, operation: state.operation, actions }),
1813
- state.status === "error" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dpu-banner", "data-tone": "error", role: "alert", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dpu-banner-row", children: [
1814
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: t("loadFailed", { reason: state.error ?? "" }) }),
1815
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Button, { size: "sm", variant: "outline", onClick: actions.refresh, children: t("retry") })
1816
- ] }) }) : null,
1817
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { className: "dpu-list", children: state.candidates.map((candidate) => {
1818
- const lookup = state.lookups[candidate.name];
1819
- const target = updateTarget(candidate, lookup);
1820
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { className: "dpu-card", "data-plugin-updates-package": candidate.name, children: [
1821
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Artwork, { src: candidate.icon }),
1822
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dpu-main", children: [
1823
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dpu-name-row", children: [
1824
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "dpu-name", title: candidate.name, onClick: () => openPackage(candidate.name), children: candidate.title }),
1825
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StatusTag, { t, candidate, lookup })
1826
- ] }),
1827
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "dpu-meta", children: [
1828
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dpu-mono", children: target === void 0 ? candidate.version : t("version.arrow", { from: candidate.version, to: target }) }),
1829
- lookup?.status === "error" ? ` \xB7 ${lookup.reason}` : ""
1830
- ] })
1831
- ] }),
1832
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dpu-end", children: [
1833
- target !== void 0 && candidate.name !== SELF ? /* @__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: () => {
1834
- void actions.install(candidate.name, target);
1835
- }, children: t("update") }) : null,
1836
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Button, { size: "sm", variant: "ghost", onClick: () => openPackage(candidate.name), children: t("details") })
1837
- ] })
1838
- ] }, candidate.name);
1839
- }) }),
1840
- state.skipped > 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dpu-caption", children: t("summary.skipped", { count: state.skipped }) }) : null,
1841
- selfTarget === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelfNotice, { t, version: selfTarget })
1842
- ] });
1843
- }
1844
1807
  function useSubject({ subject, useUpdates, actions }) {
1845
1808
  const name2 = subject.kind === "bundle" ? subject.pkg.name : void 0;
1846
1809
  const candidate = useUpdates((state) => state.candidates.find((item) => item.name === name2));
@@ -1888,7 +1851,7 @@ function DetailSection(props) {
1888
1851
  const self = candidate.name === SELF;
1889
1852
  const target = updateTarget(candidate, lookup);
1890
1853
  let body;
1891
- if (lookup === void 0 || lookup.status === "loading") body = /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dpu-caption", children: t("summary.loading") });
1854
+ if (lookup === void 0 || lookup.status === "loading") body = /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dpu-caption", children: t("versions.loading") });
1892
1855
  else if (lookup.status === "error") body = /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dpu-error", children: t("versions.failed", { reason: lookup.reason }) });
1893
1856
  else if (lookup.versions.length === 0) body = /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dpu-caption", children: t("versions.empty") });
1894
1857
  else {
@@ -1940,6 +1903,13 @@ function ListControls({ t, useUpdates, actions }) {
1940
1903
  if (target !== void 0 && candidate.name !== SELF) targets.set(candidate.name, target);
1941
1904
  }
1942
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
+ }
1943
1913
  if (anchors.head !== void 0 && targets.size > 0) {
1944
1914
  portals.push((0, import_react_dom.createPortal)(
1945
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: () => {
@@ -1983,7 +1953,6 @@ function apply(ctx) {
1983
1953
  hooks: { updates: store },
1984
1954
  actions: {
1985
1955
  ensure: store.ensure,
1986
- refresh: store.refresh,
1987
1956
  install: store.install,
1988
1957
  updateAll: store.updateAll,
1989
1958
  restore: store.restore,
@@ -1991,14 +1960,6 @@ function apply(ctx) {
1991
1960
  toastDone: store.toastDone
1992
1961
  }
1993
1962
  };
1994
- ctx.slots.inject("plugins.tab", () => ctx.slots.register({
1995
- name: "plugins.tab",
1996
- id: "updates",
1997
- order: 10,
1998
- label: () => t("tab"),
1999
- locale: LOCALE_NS,
2000
- inject: () => face
2001
- }, UpdatesTab));
2002
1963
  ctx.slots.inject("shell.overlay", () => ctx.slots.register({
2003
1964
  name: "shell.overlay",
2004
1965
  id: "plugin-updates.toast",
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@shellraining/dsh-plugin-updates",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
- "description": "插件页的「更新」标签:检查 npm 来源插件的新版本,一键更新或安装指定版本",
5
+ "description": "在插件页检查 npm 来源插件的新版本,一键更新或安装指定版本",
6
6
  "main": "./lib/index.js",
7
7
  "exports": {
8
8
  ".": "./lib/index.js",
@@ -14,10 +14,6 @@
14
14
  "lib",
15
15
  "cordis.patch.yml"
16
16
  ],
17
- "scripts": {
18
- "typecheck": "tsc -p tsconfig.json",
19
- "build": "pnpm run typecheck && node build.mjs"
20
- },
21
17
  "engines": {
22
18
  "node": ">=20"
23
19
  },
@@ -59,5 +55,9 @@
59
55
  ],
60
56
  "publishConfig": {
61
57
  "access": "public"
58
+ },
59
+ "scripts": {
60
+ "typecheck": "tsc -p tsconfig.json",
61
+ "build": "pnpm run typecheck && node build.mjs"
62
62
  }
63
- }
63
+ }