@shellraining/dsh-plugin-updates 0.1.1 → 0.2.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/README.md CHANGED
@@ -21,19 +21,33 @@ dsh plugin --profile web add @shellraining/dsh-plugin-updates
21
21
 
22
22
  插件不改 profile 文件,所有改动都调用宿主插件管理器的接口:先卸载旧版本,再安装 `包名@版本`,用精确版本号保存,并保留原来的启用状态。版本兼容检查、构建脚本审批和锁文件回滚都由宿主处理。
23
23
 
24
- 卸载后安装失败时,插件会保持未安装状态,提示里会显示 pnpm 的输出,并提供「恢复」按钮,按原来的版本重新安装。发布不足 24 小时的版本会标为「新发布」,pnpm 开启 `minimumReleaseAge` 时,这类版本可能装不上。
24
+ 操作前会重新读取安装状态,已经是目标版本时跳过;精确版本声明与实际安装版本不一致时,会停止更新,提示先修复安装。卸载失败后不会继续安装,也不会断言安装状态完全没变。
25
25
 
26
- 本插件不能在页面里更新自己,因为卸载自己时界面也会一起卸载。有新版本时,它的详情页会给出对应的 `dsh plugin` 命令,退出 DSH 后在终端运行即可。
26
+ 卸载后安装失败时,已安装列表下方会保留完整错误、日志路径和「恢复」按钮,按原来的版本重新安装;恢复失败后可以再次重试。只有确认卸载成功后才提供恢复,不会覆盖随后通过其他入口安装的版本。错误摘要优先显示错误行,不会把 pnpm 开头的成功检查信息当成失败原因。
27
+
28
+ 发布不足 24 小时的版本会标为「新发布」,pnpm 开启 `minimumReleaseAge` 时,这类版本可能装不上。如果出现 `ERR_PNPM_RESOLUTION_POLICY_VIOLATIONS_UNHANDLED`,说明 pnpm 的策略处理发生内部错误,不代表供应链校验通过就能继续安装;插件会停止操作并保留日志,不会关闭安全策略。
27
29
 
28
30
  官方列表没有给卡片开放扩展点,列表上的按钮靠 `data-plugin-group="bundles"`、`data-plugin-package` 和开关的 `role="switch"` 这几个语义属性定位,见 `src/client/anchors.ts`。DSH 改了这部分结构时,列表上的按钮会消失,详情页不受影响。
29
31
 
32
+ ## 更新本插件自己
33
+
34
+ 在页面里卸载本插件时,负责后续安装的浏览器代码也会被卸掉,所以本插件的更新交给它的宿主半侧执行。详情页有新版本时会出现「更新到 x.y.z」,确认后浏览器只发一个请求,宿主在后台依次完成:
35
+
36
+ 1. 卸载当前版本,再按精确版本安装新版本,保持启用。
37
+ 2. 新版本装不上时,装回原来的版本,并保留新版本的错误和日志路径。
38
+ 3. 卸载这一步失败时,把被取消选中的插件重新启用。
39
+
40
+ 宿主保存的是插件管理器服务本身,本插件的上下文在卸载时释放,不影响后续步骤。进度写在 profile 的 `.dsh-plugin-updates/self-update.json`,新版本加载后读取这个文件,提示一次结果。更新期间插件页上本插件的按钮会暂时消失,装好后自动回来。
41
+
42
+ 接口挂在宿主 Connection 的 `/api/dsh-plugin-updates.self-update` 上,沿用宿主的登录 cookie 和同源检查,只接受精确版本号,包名在代码里固定为本插件。确认对话框里也列出了 `dsh plugin` 命令,原版本也装不回时,可以退出 DSH 后在终端手动安装。
43
+
30
44
  ## 开发
31
45
 
32
46
  ```sh
33
47
  pnpm --filter @shellraining/dsh-plugin-updates build
34
48
  ```
35
49
 
36
- 只有浏览器半侧,源码在 `src/client/`。semver 打包进 `lib/client.js`,React、React DOM 和 `@deepseek-ai/dsh-client-ui-primitives` 由宿主提供。
50
+ 宿主半侧在 `src/host/`,只负责自更新;浏览器半侧在 `src/client/`,两边共用的接口约定在 `src/shared/`。semver 打包进 `lib/client.js`,React、React DOM 和 `@deepseek-ai/dsh-client-ui-primitives` 由宿主提供。
37
51
 
38
52
  ## 许可
39
53
 
package/cordis.patch.yml CHANGED
@@ -1,4 +1,4 @@
1
- # 插件页的「更新」标签与插件详情页的版本区块,只有浏览器半侧。
1
+ # 宿主半侧提供自更新接口;插件页的更新按钮与详情页的版本区块在浏览器半侧。
2
2
  - insert:
3
3
  - id: plugin-updates
4
4
  name: '@shellraining/dsh-plugin-updates'
package/lib/client.js CHANGED
@@ -4,7 +4,6 @@ window.__ModuleLoader__.load({
4
4
  factory: (require) => {
5
5
  var module = { exports: {} };
6
6
  var exports = module.exports;
7
- "use strict";
8
7
  var __create = Object.create;
9
8
  var __defProp = Object.defineProperty;
10
9
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -1285,17 +1284,29 @@ var ZH = {
1285
1284
  "tag.fresh": "\u65B0\u53D1\u5E03",
1286
1285
  "hint.fresh": "\u53D1\u5E03\u4E0D\u8DB3 24 \u5C0F\u65F6\uFF1Apnpm \u9ED8\u8BA4\u4E0D\u5B89\u88C5\u8FD9\u4E48\u65B0\u7684\u7248\u672C\uFF0C\u5B89\u88C5\u53EF\u80FD\u5931\u8D25",
1287
1286
  "hint.incompatible": "\u4E0E\u5F53\u524D DSH \u7248\u672C\u4E0D\u517C\u5BB9\uFF0C\u5B89\u88C5\u4F1A\u88AB\u62D2\u7EDD",
1288
- "self.title": "\u672C\u63D2\u4EF6\u4E0D\u80FD\u5728\u9875\u9762\u91CC\u66F4\u65B0\u81EA\u5DF1",
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",
1287
+ "self.body": "\u53EF\u4EE5\u66F4\u65B0\u5230 {version}\u3002\u66F4\u65B0\u5728\u5BBF\u4E3B\u540E\u53F0\u8FDB\u884C\uFF0C\u671F\u95F4\u672C\u63D2\u4EF6\u7684\u754C\u9762\u4F1A\u6682\u65F6\u6D88\u5931",
1288
+ "self.confirmTitle": "\u628A\u672C\u63D2\u4EF6\u66F4\u65B0\u5230 {version}\uFF1F",
1289
+ "self.confirmBody": "\u5BBF\u4E3B\u4F1A\u5148\u5378\u8F7D\u5F53\u524D\u7248\u672C\uFF0C\u518D\u5B89\u88C5\u65B0\u7248\u672C\u3002\u671F\u95F4\u63D2\u4EF6\u9875\u4E0A\u7684\u66F4\u65B0\u6309\u94AE\u4F1A\u6682\u65F6\u6D88\u5931\uFF0C\u88C5\u597D\u540E\u81EA\u52A8\u56DE\u6765\uFF1B\u65B0\u7248\u672C\u88C5\u4E0D\u4E0A\u65F6\u4F1A\u88C5\u56DE\u5F53\u524D\u7248\u672C\u3002\u5982\u679C\u8FDE\u5F53\u524D\u7248\u672C\u4E5F\u88C5\u4E0D\u56DE\uFF0C\u53EF\u4EE5\u9000\u51FA DSH \u540E\u5728\u7EC8\u7AEF\u8FD0\u884C\u4E0B\u9762\u7684\u547D\u4EE4\u624B\u52A8\u5B89\u88C5\uFF1A",
1290
+ "self.cancel": "\u53D6\u6D88",
1290
1291
  "self.copy": "\u590D\u5236\u547D\u4EE4",
1291
1292
  "self.copied": "\u5DF2\u590D\u5236",
1293
+ "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",
1294
+ "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",
1295
+ "error.stateChanged": "\u63D2\u4EF6\u5B89\u88C5\u72B6\u6001\u5DF2\u53D8\u5316\uFF0C\u8BF7\u5237\u65B0\u63D2\u4EF6\u5217\u8868\u540E\u91CD\u8BD5",
1296
+ "error.interrupted": "\u5BBF\u4E3B\u5728\u66F4\u65B0\u5B8C\u6210\u524D\u9000\u51FA\u4E86\uFF0C\u8BF7\u68C0\u67E5\u63D2\u4EF6\u5217\u8868\u91CC\u7684\u7248\u672C",
1297
+ "error.selfBusy": "\u5DF2\u6709\u4E00\u4E2A\u81EA\u66F4\u65B0\u5728\u8FDB\u884C",
1298
+ "op.checking": "\u6B63\u5728\u6838\u5BF9 {name} \u7684\u5B89\u88C5\u72B6\u6001\u2026",
1299
+ "op.checkFailed": "\u65E0\u6CD5\u786E\u8BA4 {name} \u7684\u5B89\u88C5\u72B6\u6001\uFF0C\u5DF2\u505C\u6B62\u64CD\u4F5C\uFF1A{reason}",
1300
+ "op.current": "{name} \u5DF2\u662F {to}\uFF0C\u65E0\u9700\u91CD\u590D\u5B89\u88C5",
1292
1301
  "op.removing": "\u6B63\u5728\u5378\u8F7D {name} {from}\u2026",
1293
1302
  "op.installing": "\u6B63\u5728\u5B89\u88C5 {name} {to}\u2026",
1294
1303
  "op.restoring": "\u6B63\u5728\u6062\u590D {name} {to}\u2026",
1295
1304
  "op.done": "{name} \u5DF2\u5207\u6362\u5230 {to}",
1296
1305
  "op.restart": "{name} \u5DF2\u5207\u6362\u5230 {to}\uFF0C\u91CD\u542F DSH \u540E\u751F\u6548",
1297
1306
  "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}",
1307
+ "op.restored": "{name} \u5B89\u88C5 {to} \u5931\u8D25\uFF0C\u5DF2\u88C5\u56DE {from}\uFF1A{reason}",
1308
+ "op.selfFailed": "{name} \u66F4\u65B0\u5230 {to} \u5931\u8D25\uFF1A{reason}",
1309
+ "op.removeFailed": "{name} \u5378\u8F7D\u672A\u5B8C\u6210\uFF0C\u5DF2\u505C\u6B62\u540E\u7EED\u5B89\u88C5\uFF1A{reason}",
1299
1310
  "op.restore": "\u6062\u590D {from}",
1300
1311
  "op.dismiss": "\u5173\u95ED",
1301
1312
  "op.log": "\u65E5\u5FD7\uFF1A{path}"
@@ -1321,17 +1332,29 @@ var EN = {
1321
1332
  "tag.fresh": "New",
1322
1333
  "hint.fresh": "Published less than 24 hours ago: pnpm does not install versions this new by default, so installing may fail",
1323
1334
  "hint.incompatible": "Not compatible with this DSH version; the installation would be refused",
1324
- "self.title": "This plugin cannot update itself from the page",
1325
- "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:",
1335
+ "self.body": "Version {version} is available. The host updates this plugin in the background, and its controls disappear meanwhile",
1336
+ "self.confirmTitle": "Update this plugin to {version}?",
1337
+ "self.confirmBody": "The host uninstalls the current version, then installs the new one. The update controls on the Plugins page disappear meanwhile and come back once it is installed; if the new version cannot be installed, the current one is put back. If even that fails, quit DSH and run this command in a terminal to install it by hand:",
1338
+ "self.cancel": "Cancel",
1326
1339
  "self.copy": "Copy command",
1327
1340
  "self.copied": "Copied",
1341
+ "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",
1342
+ "error.inconsistent": "The declared and installed versions differ. Repair the installation with the host plugin manager or CLI before retrying",
1343
+ "error.stateChanged": "The installed state has changed. Refresh the plugin list and retry",
1344
+ "error.interrupted": "The host stopped before the update finished; check the version in the plugin list",
1345
+ "error.selfBusy": "A self-update is already running",
1346
+ "op.checking": "Checking the installed version of {name}\u2026",
1347
+ "op.checkFailed": "Could not confirm the installed state of {name}; stopped: {reason}",
1348
+ "op.current": "{name} is already at {to}; no reinstall needed",
1328
1349
  "op.removing": "Uninstalling {name} {from}\u2026",
1329
1350
  "op.installing": "Installing {name} {to}\u2026",
1330
1351
  "op.restoring": "Restoring {name} {to}\u2026",
1331
1352
  "op.done": "{name} is now at {to}",
1332
1353
  "op.restart": "{name} is now at {to}; restart DSH to apply it",
1333
1354
  "op.failed": "Could not install {name} {to}: {reason}",
1334
- "op.removeFailed": "Could not uninstall {name}; nothing changed: {reason}",
1355
+ "op.restored": "Could not install {name} {to}; {from} was put back: {reason}",
1356
+ "op.selfFailed": "Could not update {name} to {to}: {reason}",
1357
+ "op.removeFailed": "Uninstalling {name} did not finish; installation was not started: {reason}",
1335
1358
  "op.restore": "Restore {from}",
1336
1359
  "op.dismiss": "Dismiss",
1337
1360
  "op.log": "Log: {path}"
@@ -1344,17 +1367,30 @@ var import_rcompare = __toESM(require_rcompare(), 1);
1344
1367
  var import_satisfies = __toESM(require_satisfies(), 1);
1345
1368
  var import_valid = __toESM(require_valid(), 1);
1346
1369
  var import_valid2 = __toESM(require_valid2(), 1);
1370
+
1371
+ // src/shared/protocol.ts
1347
1372
  var SELF = "@shellraining/dsh-plugin-updates";
1373
+ var SELF_UPDATE_PATH = "/api/dsh-plugin-updates.self-update";
1374
+ function isRunning(status) {
1375
+ return status?.phase === "removing" || status?.phase === "installing" || status?.phase === "restoring";
1376
+ }
1377
+ function errorLine(text) {
1378
+ return text.split("\n").map((line) => line.trim()).filter(Boolean).find((line) => /\bERR[_!\s]|\berror\b|\bfatal\b/i.test(line));
1379
+ }
1380
+
1381
+ // src/client/model.ts
1348
1382
  var DEFAULT_REGISTRY = "https://registry.npmjs.org/";
1349
1383
  var CACHE_MS = 10 * 6e4;
1350
1384
  var FRESH_MS = 24 * 60 * 6e4;
1385
+ var POLL_MS = 1e3;
1386
+ var SEEN_KEY = `${SELF}:self-update-seen`;
1351
1387
  function updateTarget(candidate, lookup) {
1352
1388
  if (lookup?.status !== "ready") return void 0;
1353
1389
  const best = lookup.versions.find((info) => info.compatible && !info.prerelease && !info.deprecated);
1354
1390
  return best !== void 0 && (0, import_gt.default)(best.version, candidate.version) ? best.version : void 0;
1355
1391
  }
1356
1392
  function isBusy(operation) {
1357
- return operation?.phase === "removing" || operation?.phase === "installing";
1393
+ return operation?.phase === "checking" || operation?.phase === "removing" || operation?.phase === "installing";
1358
1394
  }
1359
1395
  function record(value) {
1360
1396
  return typeof value === "object" && value !== null ? value : {};
@@ -1362,6 +1398,53 @@ function record(value) {
1362
1398
  function errorText(error) {
1363
1399
  return error instanceof Error ? error.message : String(error);
1364
1400
  }
1401
+ function failureReason(result) {
1402
+ if (!result.ok) return result.error.message;
1403
+ const { error, packageResult, application } = result.value;
1404
+ const diagnostic = error?.diagnostic?.trim();
1405
+ const lines = diagnostic || packageResult?.output || "";
1406
+ return errorLine(lines) ?? error?.code ?? diagnostic ?? application;
1407
+ }
1408
+ async function selfRequest(init) {
1409
+ const response = await fetch(SELF_UPDATE_PATH.slice(1), { credentials: "same-origin", cache: "no-store", ...init });
1410
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
1411
+ const body = record(await response.json());
1412
+ if (body["ok"] === true) {
1413
+ const status = record(body["status"]);
1414
+ const valid2 = typeof status["id"] === "string" && typeof status["from"] === "string" && typeof status["to"] === "string" && typeof status["phase"] === "string";
1415
+ return { ok: true, status: valid2 ? status : null };
1416
+ }
1417
+ const code = body["code"];
1418
+ return { ok: false, code: code === "invalid-version" || code === "busy" || code === "not-npm" || code === "current" ? code : "internal", ...typeof body["message"] === "string" ? { message: body["message"] } : {} };
1419
+ }
1420
+ function selfOperation(status, toast) {
1421
+ const phase = status.phase === "restoring" ? "installing" : status.phase;
1422
+ return {
1423
+ name: SELF,
1424
+ from: status.from,
1425
+ to: status.phase === "restoring" ? status.from : status.to,
1426
+ enabled: true,
1427
+ phase,
1428
+ restoring: status.phase === "restoring",
1429
+ ...status.reason === void 0 ? {} : { reason: status.reason },
1430
+ ...status.output === void 0 ? {} : { output: status.output },
1431
+ ...status.logPath === void 0 ? {} : { logPath: status.logPath },
1432
+ ...toast ? { toast: Date.now() } : {}
1433
+ };
1434
+ }
1435
+ function seen(id) {
1436
+ try {
1437
+ return localStorage.getItem(SEEN_KEY) === id;
1438
+ } catch {
1439
+ return false;
1440
+ }
1441
+ }
1442
+ function markSeen(id) {
1443
+ try {
1444
+ localStorage.setItem(SEEN_KEY, id);
1445
+ } catch {
1446
+ }
1447
+ }
1365
1448
  function npmInstalled(bundle) {
1366
1449
  const prefix = `${bundle.name}@`;
1367
1450
  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;
@@ -1392,6 +1475,7 @@ var UpdatesStore = class {
1392
1475
  disposed = false;
1393
1476
  registries;
1394
1477
  exemptions = {};
1478
+ pollTimer;
1395
1479
  getSnapshot = () => this.state;
1396
1480
  subscribe = (listener) => {
1397
1481
  this.listeners.add(listener);
@@ -1401,8 +1485,59 @@ var UpdatesStore = class {
1401
1485
  };
1402
1486
  dispose() {
1403
1487
  this.disposed = true;
1488
+ if (this.pollTimer !== void 0) clearTimeout(this.pollTimer);
1404
1489
  this.listeners.clear();
1405
1490
  }
1491
+ /** Read the last self-update once per load: report a result this page has not shown, follow one still running. */
1492
+ watchSelf = async () => {
1493
+ let response;
1494
+ try {
1495
+ response = await selfRequest();
1496
+ } catch {
1497
+ if (!this.disposed && this.state.operation?.name === SELF && isBusy(this.state.operation)) {
1498
+ this.pollTimer = setTimeout(() => {
1499
+ void this.watchSelf();
1500
+ }, POLL_MS);
1501
+ }
1502
+ return;
1503
+ }
1504
+ if (this.disposed || !response.ok || response.status === null) return;
1505
+ const status = response.status;
1506
+ if (isRunning(status)) {
1507
+ this.patch({ operation: selfOperation(status, false) });
1508
+ this.pollTimer = setTimeout(() => {
1509
+ void this.watchSelf();
1510
+ }, POLL_MS);
1511
+ return;
1512
+ }
1513
+ if (seen(status.id)) return;
1514
+ markSeen(status.id);
1515
+ this.patch({ operation: selfOperation(status, true) });
1516
+ };
1517
+ /** Ask the host half to update this plugin; the page loses this plugin while it runs. */
1518
+ updateSelf = async (version) => {
1519
+ if (this.disposed || isBusy(this.state.operation) || (0, import_valid.default)(version) === null) return false;
1520
+ const from = this.state.candidates.find((item) => item.name === SELF)?.version ?? "";
1521
+ const base = { name: SELF, from, to: version, enabled: true, restoring: false };
1522
+ this.patch({ operation: { ...base, phase: "checking" } });
1523
+ let response;
1524
+ try {
1525
+ response = await selfRequest({ method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ version }) });
1526
+ } catch (error) {
1527
+ response = { ok: false, code: "internal", message: errorText(error) };
1528
+ }
1529
+ if (this.disposed) return false;
1530
+ if (!response.ok) {
1531
+ const phase = response.code === "current" ? "current" : "check-failed";
1532
+ this.patch({ operation: { ...base, phase, reason: response.message ?? `self-${response.code}`, toast: Date.now() } });
1533
+ return false;
1534
+ }
1535
+ if (response.status !== null) this.patch({ operation: selfOperation(response.status, false) });
1536
+ this.pollTimer = setTimeout(() => {
1537
+ void this.watchSelf();
1538
+ }, POLL_MS);
1539
+ return true;
1540
+ };
1406
1541
  patch(next) {
1407
1542
  if (this.disposed) return;
1408
1543
  this.state = { ...this.state, ...next };
@@ -1422,6 +1557,13 @@ var UpdatesStore = class {
1422
1557
  if (this.state.status !== "idle") void this.load();
1423
1558
  };
1424
1559
  async load() {
1560
+ try {
1561
+ await this.loadBundles();
1562
+ } catch {
1563
+ if (!this.disposed) this.patch({ status: "error" });
1564
+ }
1565
+ }
1566
+ async loadBundles() {
1425
1567
  const generation = ++this.generation;
1426
1568
  this.patch({ status: this.state.status === "ready" ? "ready" : "loading" });
1427
1569
  const pm = this.ctx.remote.pluginManager;
@@ -1495,14 +1637,14 @@ var UpdatesStore = class {
1495
1637
  /** Switch one plugin to `version`: uninstall it, then install that exact version with its enabled state. */
1496
1638
  install = async (name2, version) => {
1497
1639
  const candidate = this.state.candidates.find((item) => item.name === name2);
1498
- if (candidate === void 0 || name2 === SELF || isBusy(this.state.operation)) return false;
1640
+ if (this.disposed || candidate === void 0 || name2 === SELF || isBusy(this.state.operation) || (0, import_valid.default)(version) === null) return false;
1499
1641
  return this.run({ name: name2, from: candidate.version, to: version, enabled: candidate.enabled, restoring: false }, true);
1500
1642
  };
1501
1643
  /** Put back the version a failed switch removed; the plugin is no longer installed, so only install. */
1502
1644
  restore = async () => {
1503
1645
  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);
1646
+ if (this.disposed || failed?.phase !== "failed" || !failed.recoverable) return;
1647
+ await this.run({ name: failed.name, from: failed.from, to: failed.from, enabled: failed.enabled, restoring: true }, false);
1506
1648
  };
1507
1649
  /** The outcome toast finished; the banner in the tab keeps the result. */
1508
1650
  toastDone = () => {
@@ -1525,34 +1667,75 @@ var UpdatesStore = class {
1525
1667
  };
1526
1668
  async run(base, remove) {
1527
1669
  const pm = this.ctx.remote.pluginManager;
1528
- const fail = (phase, result) => {
1670
+ let failurePhase = "check-failed";
1671
+ let recoverable = false;
1672
+ const fail = (result) => {
1529
1673
  const value = result.ok ? result.value : void 0;
1530
- const output = value?.packageResult?.output.trim();
1674
+ const output = value?.packageResult?.output.trim() || value?.error?.diagnostic;
1531
1675
  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 } : {} } });
1676
+ this.patch({ operation: {
1677
+ ...base,
1678
+ phase: failurePhase,
1679
+ recoverable,
1680
+ reason: failureReason(result),
1681
+ toast: Date.now(),
1682
+ ...output ? { output } : {},
1683
+ ...logPath ? { logPath } : {}
1684
+ } });
1534
1685
  return false;
1535
1686
  };
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);
1687
+ this.patch({ operation: { ...base, phase: "checking" } });
1688
+ try {
1689
+ const listed = await pm.listBundles();
1690
+ if (this.disposed) return false;
1691
+ if (!listed.ok) return fail(listed);
1692
+ const current = listed.value.find((bundle) => bundle.name === base.name && bundle.installed);
1693
+ if (current !== void 0 && (0, import_valid.default)(current.version ?? "") === (0, import_valid.default)(base.to)) {
1694
+ this.patch({ operation: { ...base, phase: "current", toast: Date.now() } });
1695
+ return true;
1542
1696
  }
1697
+ if (remove) {
1698
+ if (current === void 0 || !npmInstalled(current)) {
1699
+ return fail({ ok: true, value: { application: "failed", error: { code: "plugin-state-changed" } } });
1700
+ }
1701
+ const declared = (0, import_valid.default)(current.source?.slice(`${base.name}@`.length) ?? "");
1702
+ if (declared !== null && declared !== (0, import_valid.default)(current.version ?? "")) {
1703
+ return fail({ ok: true, value: { application: "failed", error: { code: "installation-inconsistent" } } });
1704
+ }
1705
+ base = { ...base, from: current.version ?? base.from, enabled: current.enabled };
1706
+ failurePhase = "remove-failed";
1707
+ this.patch({ operation: { ...base, phase: "removing" } });
1708
+ const removed = await pm.removeBundle(base.name);
1709
+ const stopProfile = removed.ok && removed.value.error?.code === "stop-profile";
1710
+ if (!stopProfile && (!removed.ok || removed.value.application === "failed" || removed.value.application === "cancelled")) {
1711
+ return fail(removed);
1712
+ }
1713
+ recoverable = !stopProfile;
1714
+ } else {
1715
+ if (current !== void 0) {
1716
+ return fail({ ok: true, value: { application: "failed", error: { code: "plugin-state-changed" } } });
1717
+ }
1718
+ recoverable = true;
1719
+ }
1720
+ if (this.disposed) return false;
1721
+ failurePhase = "failed";
1722
+ this.patch({ operation: { ...base, phase: "installing" } });
1723
+ const installed = await pm.installBundle(`${base.name}@${base.to}`, {
1724
+ enabled: base.enabled,
1725
+ saveExact: true,
1726
+ requestId: `${SELF}:${Date.now()}`
1727
+ });
1728
+ if (!installed.ok || installed.value.application === "failed" || installed.value.application === "cancelled") {
1729
+ return fail(installed);
1730
+ }
1731
+ const to = installed.value.version ?? base.to;
1732
+ this.patch({ operation: { ...base, to, toast: Date.now(), phase: installed.value.application === "restart-required" ? "restart" : "done" } });
1733
+ return true;
1734
+ } catch (error) {
1735
+ return fail({ ok: false, error: { message: errorText(error) } });
1736
+ } finally {
1737
+ if (!this.disposed) await this.load();
1543
1738
  }
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
1739
  }
1557
1740
  };
1558
1741
 
@@ -1618,10 +1801,11 @@ function scan(document2) {
1618
1801
  const end = card.querySelector('[role="switch"]')?.parentElement;
1619
1802
  if (name2 && end) cards.set(name2, mountIn(end, "card", true));
1620
1803
  }
1621
- return { ...head === void 0 ? {} : { head }, cards };
1804
+ const status = mountIn(group, "status", false);
1805
+ return { ...head === void 0 ? {} : { head }, status, cards };
1622
1806
  }
1623
1807
  function same(a, b) {
1624
- if (a.head !== b.head || a.cards.size !== b.cards.size) return false;
1808
+ if (a.head !== b.head || a.status !== b.status || a.cards.size !== b.cards.size) return false;
1625
1809
  for (const [name2, node] of a.cards) if (b.cards.get(name2) !== node) return false;
1626
1810
  return true;
1627
1811
  }
@@ -1660,23 +1844,56 @@ function selfCommand(version) {
1660
1844
  function dateText(stamp) {
1661
1845
  return stamp === void 0 ? "" : new Date(stamp).toLocaleDateString();
1662
1846
  }
1663
- function SelfNotice({ t, version }) {
1847
+ function SelfNotice({ t, version, busy, onUpdate }) {
1848
+ const [open, setOpen] = (0, import_react.useState)(false);
1664
1849
  const [copied, setCopied] = (0, import_react.useState)(false);
1665
1850
  const command = selfCommand(version);
1851
+ const close = () => {
1852
+ setOpen(false);
1853
+ };
1666
1854
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dpu-banner", children: [
1667
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: t("self.title") }),
1668
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: t("self.body") }),
1669
1855
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dpu-banner-row", children: [
1670
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("pre", { className: "dpu-pre dpu-command", children: command }),
1671
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Button, { size: "sm", variant: "outline", onClick: () => {
1672
- void (0, import_dsh_client_ui_primitives.writeClipboard)(command).then(setCopied);
1673
- }, children: copied ? t("self.copied") : t("self.copy") })
1674
- ] })
1856
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: t("self.body", { version }) }),
1857
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Button, { size: "sm", variant: "primary", disabled: busy, onClick: () => {
1858
+ setOpen(true);
1859
+ }, children: t("updateTo", { version }) })
1860
+ ] }),
1861
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1862
+ import_dsh_client_ui_primitives.Modal,
1863
+ {
1864
+ open,
1865
+ onClose: close,
1866
+ title: t("self.confirmTitle", { version }),
1867
+ description: t("self.confirmBody"),
1868
+ closeLabel: t("op.dismiss"),
1869
+ footer: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1870
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Button, { size: "sm", variant: "ghost", onClick: close, children: t("self.cancel") }),
1871
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Button, { size: "sm", variant: "primary", "data-modal-autofocus": "", onClick: () => {
1872
+ close();
1873
+ onUpdate();
1874
+ }, children: t("updateTo", { version }) })
1875
+ ] }),
1876
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dpu-banner-row", children: [
1877
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("pre", { className: "dpu-pre dpu-command", children: command }),
1878
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Button, { size: "sm", variant: "outline", onClick: () => {
1879
+ void (0, import_dsh_client_ui_primitives.writeClipboard)(command).then(setCopied);
1880
+ }, children: copied ? t("self.copied") : t("self.copy") })
1881
+ ] })
1882
+ }
1883
+ )
1675
1884
  ] });
1676
1885
  }
1677
1886
  function operationText(t, operation) {
1678
- const params = { name: operation.name, from: operation.from, to: operation.to, reason: operation.reason ?? "" };
1887
+ const raw = operation.reason ?? "";
1888
+ 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 === "interrupted" ? t("error.interrupted") : raw === "self-busy" ? t("error.selfBusy") : raw === "self-not-npm" ? t("error.stateChanged") : raw;
1889
+ const params = { name: operation.name, from: operation.from, to: operation.to, reason };
1679
1890
  switch (operation.phase) {
1891
+ case "checking":
1892
+ return t("op.checking", params);
1893
+ case "check-failed":
1894
+ return t("op.checkFailed", params);
1895
+ case "current":
1896
+ return t("op.current", params);
1680
1897
  case "removing":
1681
1898
  return t("op.removing", params);
1682
1899
  case "installing":
@@ -1685,17 +1902,22 @@ function operationText(t, operation) {
1685
1902
  return t("op.done", params);
1686
1903
  case "restart":
1687
1904
  return t("op.restart", params);
1905
+ case "restored":
1906
+ return t("op.restored", params);
1688
1907
  case "failed":
1689
- return t("op.failed", params);
1908
+ return t(operation.name === SELF ? "op.selfFailed" : "op.failed", params);
1690
1909
  case "remove-failed":
1691
1910
  return t("op.removeFailed", params);
1692
1911
  }
1693
1912
  }
1913
+ function isFailure(operation) {
1914
+ return operation.phase === "failed" || operation.phase === "remove-failed" || operation.phase === "check-failed" || operation.phase === "restored";
1915
+ }
1694
1916
  function OperationToast({ t, useUpdates, actions }) {
1695
1917
  const operation = useUpdates((state) => state.operation);
1696
1918
  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;
1919
+ const failed = isFailure(operation);
1920
+ const restorable = operation.phase === "failed" && operation.recoverable;
1699
1921
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1700
1922
  import_dsh_client_ui_primitives.Toast,
1701
1923
  {
@@ -1713,12 +1935,12 @@ function OperationToast({ t, useUpdates, actions }) {
1713
1935
  function OperationBanner({ t, operation, actions }) {
1714
1936
  const params = { from: operation.from };
1715
1937
  const busy = isBusy(operation);
1716
- const failed = operation.phase === "failed" || operation.phase === "remove-failed";
1938
+ const failed = isFailure(operation);
1717
1939
  const text = operationText(t, operation);
1718
1940
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dpu-banner", role: failed ? "alert" : "status", ...failed ? { "data-tone": "error" } : {}, children: [
1719
1941
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dpu-banner-row", children: [
1720
1942
  /* @__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: () => {
1943
+ operation.phase === "failed" && operation.recoverable ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Button, { size: "sm", variant: "outline", onClick: () => {
1722
1944
  void actions.restore();
1723
1945
  }, children: t("op.restore", params) }) : null,
1724
1946
  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") })
@@ -1805,7 +2027,9 @@ function DetailSection(props) {
1805
2027
  ] })
1806
2028
  ] }),
1807
2029
  operation?.name === candidate.name ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(OperationBanner, { t, operation, actions }) : null,
1808
- self && target !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelfNotice, { t, version: target }) : null,
2030
+ self && target !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelfNotice, { t, version: target, busy: isBusy(operation), onUpdate: () => {
2031
+ void actions.updateSelf(target);
2032
+ } }) : null,
1809
2033
  body
1810
2034
  ] });
1811
2035
  }
@@ -1826,6 +2050,13 @@ function ListControls({ t, useUpdates, actions }) {
1826
2050
  if (target !== void 0 && candidate.name !== SELF) targets.set(candidate.name, target);
1827
2051
  }
1828
2052
  const portals = [];
2053
+ if (anchors.status !== void 0 && operation !== void 0) {
2054
+ portals.push((0, import_react_dom.createPortal)(
2055
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(OperationBanner, { t, operation, actions }),
2056
+ anchors.status,
2057
+ "operation"
2058
+ ));
2059
+ }
1829
2060
  if (anchors.head !== void 0 && targets.size > 0) {
1830
2061
  portals.push((0, import_react_dom.createPortal)(
1831
2062
  /* @__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: () => {
@@ -1864,12 +2095,14 @@ function apply(ctx) {
1864
2095
  for (const off of offs) off();
1865
2096
  };
1866
2097
  }, `${name}: host invalidations`);
2098
+ void store.watchSelf();
1867
2099
  const t = ctx.locale.bind(LOCALE_NS);
1868
2100
  const face = {
1869
2101
  hooks: { updates: store },
1870
2102
  actions: {
1871
2103
  ensure: store.ensure,
1872
2104
  install: store.install,
2105
+ updateSelf: store.updateSelf,
1873
2106
  updateAll: store.updateAll,
1874
2107
  restore: store.restore,
1875
2108
  dismiss: store.dismiss,
package/lib/index.js CHANGED
@@ -1,10 +1,161 @@
1
1
  // Generated by `pnpm run build`; edit src/, not this file.
2
2
 
3
- // src/index.ts
4
- var name = "@shellraining/dsh-plugin-updates";
5
- function apply() {
3
+ // src/shared/protocol.ts
4
+ var SELF = "@shellraining/dsh-plugin-updates";
5
+ var SELF_UPDATE_PATH = "/api/dsh-plugin-updates.self-update";
6
+ function isRunning(status) {
7
+ return status?.phase === "removing" || status?.phase === "installing" || status?.phase === "restoring";
8
+ }
9
+ function errorLine(text2) {
10
+ return text2.split("\n").map((line) => line.trim()).filter(Boolean).find((line) => /\bERR[_!\s]|\berror\b|\bfatal\b/i.test(line));
11
+ }
12
+
13
+ // src/host/self-update.ts
14
+ import { randomUUID } from "node:crypto";
15
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
16
+ import { join } from "node:path";
17
+ var STALE_MS = 15 * 6e4;
18
+ var VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
19
+ function record(value) {
20
+ return typeof value === "object" && value !== null ? value : {};
21
+ }
22
+ function text(error) {
23
+ return error instanceof Error ? error.message : String(error);
24
+ }
25
+ function failed(result) {
26
+ return result.application === "failed" || result.application === "cancelled";
27
+ }
28
+ function reasonOf(result) {
29
+ const output = result.error?.diagnostic ?? result.packageResult?.output ?? "";
30
+ return errorLine(output) ?? result.error?.code ?? (output.trim() || result.application);
31
+ }
32
+ function details(result) {
33
+ const output = (result.packageResult?.output.trim() || result.error?.diagnostic)?.slice(-8e3);
34
+ const logPath = result.packageResult?.logPath;
35
+ return { ...output ? { output } : {}, ...logPath ? { logPath } : {} };
36
+ }
37
+ function npmVersion(bundle) {
38
+ if (bundle === void 0 || !bundle.installed || !bundle.removable) return void 0;
39
+ if (bundle.source?.startsWith(`${SELF}@`) !== true || bundle.version === void 0) return void 0;
40
+ return VERSION.test(bundle.version) ? bundle.version : void 0;
41
+ }
42
+ var SelfUpdater = class {
43
+ constructor(profileDir, manager, logger) {
44
+ this.manager = manager;
45
+ this.logger = logger;
46
+ this.file = join(profileDir, ".dsh-plugin-updates", "self-update.json");
47
+ }
48
+ manager;
49
+ logger;
50
+ file;
51
+ running = false;
52
+ async status() {
53
+ let stored;
54
+ try {
55
+ stored = record(JSON.parse(await readFile(this.file, "utf8")));
56
+ } catch {
57
+ return null;
58
+ }
59
+ const { id, from, to, phase, startedAt } = stored;
60
+ if (typeof id !== "string" || typeof from !== "string" || typeof to !== "string" || typeof phase !== "string") return null;
61
+ const status = stored;
62
+ if (isRunning(status) && !this.running && typeof startedAt === "number" && Date.now() - startedAt > STALE_MS) {
63
+ return { ...status, phase: "failed", reason: "interrupted" };
64
+ }
65
+ return status;
66
+ }
67
+ async start(version) {
68
+ if (typeof version !== "string" || !VERSION.test(version)) return { ok: false, code: "invalid-version" };
69
+ if (this.running || isRunning(await this.status())) return { ok: false, code: "busy" };
70
+ const from = npmVersion((await this.manager.listBundles()).find((bundle) => bundle.name === SELF));
71
+ if (from === void 0) return { ok: false, code: "not-npm" };
72
+ if (from === version) return { ok: false, code: "current" };
73
+ const id = randomUUID();
74
+ const startedAt = Date.now();
75
+ const base = { id, from, to: version, startedAt };
76
+ this.running = true;
77
+ await this.save({ ...base, phase: "removing" });
78
+ void this.run(base).finally(() => {
79
+ this.running = false;
80
+ });
81
+ return { ok: true, status: { id, from, to: version, phase: "removing" } };
82
+ }
83
+ async run(base) {
84
+ const step = (phase, extra = {}) => this.save({ ...base, ...extra, phase, ...isRunning({ ...base, phase }) ? {} : { finishedAt: Date.now() } });
85
+ try {
86
+ const removed = await this.manager.removeBundle(SELF);
87
+ if (failed(removed)) {
88
+ await this.reselect();
89
+ await step("failed", { reason: reasonOf(removed), ...details(removed) });
90
+ return;
91
+ }
92
+ await step("installing");
93
+ const installed = await this.manager.installBundle(`${SELF}@${base.to}`, { enabled: true, saveExact: true });
94
+ if (!failed(installed)) {
95
+ await step(installed.application === "restart-required" ? "restart" : "done", { to: installed.version ?? base.to });
96
+ return;
97
+ }
98
+ const failure = { reason: reasonOf(installed), ...details(installed) };
99
+ await step("restoring", failure);
100
+ const restored = await this.manager.installBundle(`${SELF}@${base.from}`, { enabled: true, saveExact: true });
101
+ if (failed(restored)) {
102
+ this.logger.warn("[plugin-updates] could not restore %s@%s: %s", SELF, base.from, reasonOf(restored));
103
+ await step("failed", { ...failure, reason: `${failure.reason}
104
+ ${reasonOf(restored)}` });
105
+ return;
106
+ }
107
+ await step("restored", failure);
108
+ } catch (error) {
109
+ this.logger.warn("[plugin-updates] self-update stopped: %s", text(error));
110
+ await step("failed", { reason: text(error) }).catch(() => void 0);
111
+ }
112
+ }
113
+ async reselect() {
114
+ const bundle = (await this.manager.listBundles()).find((item) => item.name === SELF);
115
+ if (bundle?.installed === true && !bundle.enabled) await this.manager.setBundleEnabled(SELF, true);
116
+ }
117
+ async save(status) {
118
+ const dir = join(this.file, "..");
119
+ await mkdir(dir, { recursive: true, mode: 448 });
120
+ const temp = `${this.file}.${process.pid}.tmp`;
121
+ await writeFile(temp, `${JSON.stringify(status, null, 2)}
122
+ `, { mode: 384 });
123
+ await rename(temp, this.file);
124
+ }
125
+ };
126
+
127
+ // src/host/index.ts
128
+ var name = SELF;
129
+ var inject = ["connection", "pluginManager", "profileContext"];
130
+ var ORIGINAL = /* @__PURE__ */ Symbol.for("cordis.original");
131
+ function unwrap(service) {
132
+ const original = Reflect.get(service, ORIGINAL);
133
+ return typeof original === "object" && original !== null ? original : service;
134
+ }
135
+ function json(body) {
136
+ return new Response(JSON.stringify(body), { headers: { "content-type": "application/json", "cache-control": "no-store" } });
137
+ }
138
+ function apply(ctx) {
139
+ const updater = new SelfUpdater(ctx.profileContext.dir, unwrap(ctx.pluginManager), ctx.logger);
140
+ ctx.connection.fetch.register({
141
+ path: SELF_UPDATE_PATH,
142
+ methods: ["GET", "POST"],
143
+ requestBody: "buffered",
144
+ fetch: async (request) => {
145
+ try {
146
+ if (request.method === "GET") return json({ ok: true, status: await updater.status() });
147
+ const body = await request.json().catch(() => null);
148
+ const version = typeof body === "object" && body !== null ? Reflect.get(body, "version") : void 0;
149
+ return json(await updater.start(version));
150
+ } catch (error) {
151
+ ctx.logger.warn("[plugin-updates] self-update route failed: %s", error instanceof Error ? error.message : String(error));
152
+ return json({ ok: false, code: "internal", message: error instanceof Error ? error.message : String(error) });
153
+ }
154
+ }
155
+ });
6
156
  }
7
157
  export {
8
158
  apply,
159
+ inject,
9
160
  name
10
161
  };
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.2.0",
4
4
  "type": "module",
5
5
  "description": "在插件页检查 npm 来源插件的新版本,一键更新或安装指定版本",
6
6
  "main": "./lib/index.js",
@@ -19,6 +19,7 @@
19
19
  },
20
20
  "devDependencies": {
21
21
  "@deepseek-ai/dsh-client-ui-primitives": "0.2.1-rc.3",
22
+ "@types/node": "24.19.2",
22
23
  "@types/react": "18.3.31",
23
24
  "@types/react-dom": "18.3.7",
24
25
  "@types/semver": "7.8.0",
@@ -57,7 +58,7 @@
57
58
  "access": "public"
58
59
  },
59
60
  "scripts": {
60
- "typecheck": "tsc -p tsconfig.json",
61
+ "typecheck": "tsc -p tsconfig.host.json && tsc -p tsconfig.client.json",
61
62
  "build": "pnpm run typecheck && node build.mjs"
62
63
  }
63
64
  }