@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 +17 -3
- package/cordis.patch.yml +1 -1
- package/lib/client.js +283 -50
- package/lib/index.js +154 -3
- package/package.json +3 -2
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
|
-
|
|
24
|
+
操作前会重新读取安装状态,已经是目标版本时跳过;精确版本声明与实际安装版本不一致时,会停止更新,提示先修复安装。卸载失败后不会继续安装,也不会断言安装状态完全没变。
|
|
25
25
|
|
|
26
|
-
|
|
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
|
-
|
|
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
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.
|
|
1289
|
-
"self.
|
|
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.
|
|
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.
|
|
1325
|
-
"self.
|
|
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.
|
|
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.
|
|
1505
|
-
await this.run({ name: failed.name, from: failed.
|
|
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
|
-
|
|
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
|
-
|
|
1533
|
-
|
|
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
|
-
|
|
1537
|
-
|
|
1538
|
-
const
|
|
1539
|
-
|
|
1540
|
-
if (!
|
|
1541
|
-
|
|
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
|
-
|
|
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)("
|
|
1671
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Button, { size: "sm", variant: "
|
|
1672
|
-
|
|
1673
|
-
}, children:
|
|
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
|
|
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
|
|
1698
|
-
const restorable = operation.phase === "failed" &&
|
|
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
|
|
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" &&
|
|
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
|
|
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/
|
|
4
|
-
var
|
|
5
|
-
|
|
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.
|
|
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
|
}
|