@shellraining/dsh-plugin-updates 0.1.2 → 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 +13 -3
- package/cordis.patch.yml +1 -1
- package/lib/client.js +170 -21
- package/lib/index.js +154 -3
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -27,17 +27,27 @@ dsh plugin --profile web add @shellraining/dsh-plugin-updates
|
|
|
27
27
|
|
|
28
28
|
发布不足 24 小时的版本会标为「新发布」,pnpm 开启 `minimumReleaseAge` 时,这类版本可能装不上。如果出现 `ERR_PNPM_RESOLUTION_POLICY_VIOLATIONS_UNHANDLED`,说明 pnpm 的策略处理发生内部错误,不代表供应链校验通过就能继续安装;插件会停止操作并保留日志,不会关闭安全策略。
|
|
29
29
|
|
|
30
|
-
本插件不能在页面里更新自己,因为卸载自己时界面也会一起卸载。有新版本时,它的详情页会给出对应的 `dsh plugin` 命令,退出 DSH 后在终端运行即可。
|
|
31
|
-
|
|
32
30
|
官方列表没有给卡片开放扩展点,列表上的按钮靠 `data-plugin-group="bundles"`、`data-plugin-package` 和开关的 `role="switch"` 这几个语义属性定位,见 `src/client/anchors.ts`。DSH 改了这部分结构时,列表上的按钮会消失,详情页不受影响。
|
|
33
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
|
+
|
|
34
44
|
## 开发
|
|
35
45
|
|
|
36
46
|
```sh
|
|
37
47
|
pnpm --filter @shellraining/dsh-plugin-updates build
|
|
38
48
|
```
|
|
39
49
|
|
|
40
|
-
|
|
50
|
+
宿主半侧在 `src/host/`,只负责自更新;浏览器半侧在 `src/client/`,两边共用的接口约定在 `src/shared/`。semver 打包进 `lib/client.js`,React、React DOM 和 `@deepseek-ai/dsh-client-ui-primitives` 由宿主提供。
|
|
41
51
|
|
|
42
52
|
## 许可
|
|
43
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,13 +1284,17 @@ 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",
|
|
1292
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",
|
|
1293
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",
|
|
1294
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",
|
|
1295
1298
|
"op.checking": "\u6B63\u5728\u6838\u5BF9 {name} \u7684\u5B89\u88C5\u72B6\u6001\u2026",
|
|
1296
1299
|
"op.checkFailed": "\u65E0\u6CD5\u786E\u8BA4 {name} \u7684\u5B89\u88C5\u72B6\u6001\uFF0C\u5DF2\u505C\u6B62\u64CD\u4F5C\uFF1A{reason}",
|
|
1297
1300
|
"op.current": "{name} \u5DF2\u662F {to}\uFF0C\u65E0\u9700\u91CD\u590D\u5B89\u88C5",
|
|
@@ -1301,6 +1304,8 @@ var ZH = {
|
|
|
1301
1304
|
"op.done": "{name} \u5DF2\u5207\u6362\u5230 {to}",
|
|
1302
1305
|
"op.restart": "{name} \u5DF2\u5207\u6362\u5230 {to}\uFF0C\u91CD\u542F DSH \u540E\u751F\u6548",
|
|
1303
1306
|
"op.failed": "{name} \u5B89\u88C5 {to} \u5931\u8D25\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}",
|
|
1304
1309
|
"op.removeFailed": "{name} \u5378\u8F7D\u672A\u5B8C\u6210\uFF0C\u5DF2\u505C\u6B62\u540E\u7EED\u5B89\u88C5\uFF1A{reason}",
|
|
1305
1310
|
"op.restore": "\u6062\u590D {from}",
|
|
1306
1311
|
"op.dismiss": "\u5173\u95ED",
|
|
@@ -1327,13 +1332,17 @@ var EN = {
|
|
|
1327
1332
|
"tag.fresh": "New",
|
|
1328
1333
|
"hint.fresh": "Published less than 24 hours ago: pnpm does not install versions this new by default, so installing may fail",
|
|
1329
1334
|
"hint.incompatible": "Not compatible with this DSH version; the installation would be refused",
|
|
1330
|
-
"self.
|
|
1331
|
-
"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",
|
|
1332
1339
|
"self.copy": "Copy command",
|
|
1333
1340
|
"self.copied": "Copied",
|
|
1334
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",
|
|
1335
1342
|
"error.inconsistent": "The declared and installed versions differ. Repair the installation with the host plugin manager or CLI before retrying",
|
|
1336
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",
|
|
1337
1346
|
"op.checking": "Checking the installed version of {name}\u2026",
|
|
1338
1347
|
"op.checkFailed": "Could not confirm the installed state of {name}; stopped: {reason}",
|
|
1339
1348
|
"op.current": "{name} is already at {to}; no reinstall needed",
|
|
@@ -1343,6 +1352,8 @@ var EN = {
|
|
|
1343
1352
|
"op.done": "{name} is now at {to}",
|
|
1344
1353
|
"op.restart": "{name} is now at {to}; restart DSH to apply it",
|
|
1345
1354
|
"op.failed": "Could not install {name} {to}: {reason}",
|
|
1355
|
+
"op.restored": "Could not install {name} {to}; {from} was put back: {reason}",
|
|
1356
|
+
"op.selfFailed": "Could not update {name} to {to}: {reason}",
|
|
1346
1357
|
"op.removeFailed": "Uninstalling {name} did not finish; installation was not started: {reason}",
|
|
1347
1358
|
"op.restore": "Restore {from}",
|
|
1348
1359
|
"op.dismiss": "Dismiss",
|
|
@@ -1356,10 +1367,23 @@ var import_rcompare = __toESM(require_rcompare(), 1);
|
|
|
1356
1367
|
var import_satisfies = __toESM(require_satisfies(), 1);
|
|
1357
1368
|
var import_valid = __toESM(require_valid(), 1);
|
|
1358
1369
|
var import_valid2 = __toESM(require_valid2(), 1);
|
|
1370
|
+
|
|
1371
|
+
// src/shared/protocol.ts
|
|
1359
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
|
|
1360
1382
|
var DEFAULT_REGISTRY = "https://registry.npmjs.org/";
|
|
1361
1383
|
var CACHE_MS = 10 * 6e4;
|
|
1362
1384
|
var FRESH_MS = 24 * 60 * 6e4;
|
|
1385
|
+
var POLL_MS = 1e3;
|
|
1386
|
+
var SEEN_KEY = `${SELF}:self-update-seen`;
|
|
1363
1387
|
function updateTarget(candidate, lookup) {
|
|
1364
1388
|
if (lookup?.status !== "ready") return void 0;
|
|
1365
1389
|
const best = lookup.versions.find((info) => info.compatible && !info.prerelease && !info.deprecated);
|
|
@@ -1378,9 +1402,48 @@ function failureReason(result) {
|
|
|
1378
1402
|
if (!result.ok) return result.error.message;
|
|
1379
1403
|
const { error, packageResult, application } = result.value;
|
|
1380
1404
|
const diagnostic = error?.diagnostic?.trim();
|
|
1381
|
-
const lines =
|
|
1382
|
-
|
|
1383
|
-
|
|
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
|
+
}
|
|
1384
1447
|
}
|
|
1385
1448
|
function npmInstalled(bundle) {
|
|
1386
1449
|
const prefix = `${bundle.name}@`;
|
|
@@ -1412,6 +1475,7 @@ var UpdatesStore = class {
|
|
|
1412
1475
|
disposed = false;
|
|
1413
1476
|
registries;
|
|
1414
1477
|
exemptions = {};
|
|
1478
|
+
pollTimer;
|
|
1415
1479
|
getSnapshot = () => this.state;
|
|
1416
1480
|
subscribe = (listener) => {
|
|
1417
1481
|
this.listeners.add(listener);
|
|
@@ -1421,8 +1485,59 @@ var UpdatesStore = class {
|
|
|
1421
1485
|
};
|
|
1422
1486
|
dispose() {
|
|
1423
1487
|
this.disposed = true;
|
|
1488
|
+
if (this.pollTimer !== void 0) clearTimeout(this.pollTimer);
|
|
1424
1489
|
this.listeners.clear();
|
|
1425
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
|
+
};
|
|
1426
1541
|
patch(next) {
|
|
1427
1542
|
if (this.disposed) return;
|
|
1428
1543
|
this.state = { ...this.state, ...next };
|
|
@@ -1729,23 +1844,48 @@ function selfCommand(version) {
|
|
|
1729
1844
|
function dateText(stamp) {
|
|
1730
1845
|
return stamp === void 0 ? "" : new Date(stamp).toLocaleDateString();
|
|
1731
1846
|
}
|
|
1732
|
-
function SelfNotice({ t, version }) {
|
|
1847
|
+
function SelfNotice({ t, version, busy, onUpdate }) {
|
|
1848
|
+
const [open, setOpen] = (0, import_react.useState)(false);
|
|
1733
1849
|
const [copied, setCopied] = (0, import_react.useState)(false);
|
|
1734
1850
|
const command = selfCommand(version);
|
|
1851
|
+
const close = () => {
|
|
1852
|
+
setOpen(false);
|
|
1853
|
+
};
|
|
1735
1854
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dpu-banner", children: [
|
|
1736
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: t("self.title") }),
|
|
1737
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: t("self.body") }),
|
|
1738
1855
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dpu-banner-row", children: [
|
|
1739
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("
|
|
1740
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Button, { size: "sm", variant: "
|
|
1741
|
-
|
|
1742
|
-
}, children:
|
|
1743
|
-
] })
|
|
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
|
+
)
|
|
1744
1884
|
] });
|
|
1745
1885
|
}
|
|
1746
1886
|
function operationText(t, operation) {
|
|
1747
1887
|
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;
|
|
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;
|
|
1749
1889
|
const params = { name: operation.name, from: operation.from, to: operation.to, reason };
|
|
1750
1890
|
switch (operation.phase) {
|
|
1751
1891
|
case "checking":
|
|
@@ -1762,16 +1902,21 @@ function operationText(t, operation) {
|
|
|
1762
1902
|
return t("op.done", params);
|
|
1763
1903
|
case "restart":
|
|
1764
1904
|
return t("op.restart", params);
|
|
1905
|
+
case "restored":
|
|
1906
|
+
return t("op.restored", params);
|
|
1765
1907
|
case "failed":
|
|
1766
|
-
return t("op.failed", params);
|
|
1908
|
+
return t(operation.name === SELF ? "op.selfFailed" : "op.failed", params);
|
|
1767
1909
|
case "remove-failed":
|
|
1768
1910
|
return t("op.removeFailed", params);
|
|
1769
1911
|
}
|
|
1770
1912
|
}
|
|
1913
|
+
function isFailure(operation) {
|
|
1914
|
+
return operation.phase === "failed" || operation.phase === "remove-failed" || operation.phase === "check-failed" || operation.phase === "restored";
|
|
1915
|
+
}
|
|
1771
1916
|
function OperationToast({ t, useUpdates, actions }) {
|
|
1772
1917
|
const operation = useUpdates((state) => state.operation);
|
|
1773
1918
|
if (operation?.toast === void 0) return null;
|
|
1774
|
-
const failed = operation
|
|
1919
|
+
const failed = isFailure(operation);
|
|
1775
1920
|
const restorable = operation.phase === "failed" && operation.recoverable;
|
|
1776
1921
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1777
1922
|
import_dsh_client_ui_primitives.Toast,
|
|
@@ -1790,7 +1935,7 @@ function OperationToast({ t, useUpdates, actions }) {
|
|
|
1790
1935
|
function OperationBanner({ t, operation, actions }) {
|
|
1791
1936
|
const params = { from: operation.from };
|
|
1792
1937
|
const busy = isBusy(operation);
|
|
1793
|
-
const failed = operation
|
|
1938
|
+
const failed = isFailure(operation);
|
|
1794
1939
|
const text = operationText(t, operation);
|
|
1795
1940
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dpu-banner", role: failed ? "alert" : "status", ...failed ? { "data-tone": "error" } : {}, children: [
|
|
1796
1941
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dpu-banner-row", children: [
|
|
@@ -1882,7 +2027,9 @@ function DetailSection(props) {
|
|
|
1882
2027
|
] })
|
|
1883
2028
|
] }),
|
|
1884
2029
|
operation?.name === candidate.name ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(OperationBanner, { t, operation, actions }) : null,
|
|
1885
|
-
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,
|
|
1886
2033
|
body
|
|
1887
2034
|
] });
|
|
1888
2035
|
}
|
|
@@ -1948,12 +2095,14 @@ function apply(ctx) {
|
|
|
1948
2095
|
for (const off of offs) off();
|
|
1949
2096
|
};
|
|
1950
2097
|
}, `${name}: host invalidations`);
|
|
2098
|
+
void store.watchSelf();
|
|
1951
2099
|
const t = ctx.locale.bind(LOCALE_NS);
|
|
1952
2100
|
const face = {
|
|
1953
2101
|
hooks: { updates: store },
|
|
1954
2102
|
actions: {
|
|
1955
2103
|
ensure: store.ensure,
|
|
1956
2104
|
install: store.install,
|
|
2105
|
+
updateSelf: store.updateSelf,
|
|
1957
2106
|
updateAll: store.updateAll,
|
|
1958
2107
|
restore: store.restore,
|
|
1959
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
|
}
|