@restregular/dsh-powerup 0.2.3 → 0.3.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 +35 -12
- package/client.js +466 -49
- package/index.js +161 -14
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -41,20 +41,31 @@ Open the built-in **Plugins** page. Every third-party card under **Installed
|
|
|
41
41
|
plugins** gains a small update icon in its action area. Clicking it opens the
|
|
42
42
|
update dialog:
|
|
43
43
|
|
|
44
|
-
1. The dialog reads the package's published versions from the
|
|
45
|
-
**Add plugin** dialog
|
|
46
|
-
|
|
47
|
-
from the
|
|
48
|
-
|
|
44
|
+
1. The dialog reads the package's published versions from the Host's configured
|
|
45
|
+
registries — the same list the **Add plugin** dialog offers, including the
|
|
46
|
+
npmmirror fallback. Star any registry to make it Power-Up's default for
|
|
47
|
+
later updates; otherwise the list starts from the Add dialog's remembered
|
|
48
|
+
choice.
|
|
49
|
+
2. Type a version or pick one from the floating version list. The current
|
|
50
|
+
installed version is tagged **Current**; the newest is tagged **latest**, and
|
|
51
|
+
a version that is both uses one **Current · latest** tag.
|
|
52
|
+
3. Confirming removes the installed copy, checks the new spec the way **Add
|
|
49
53
|
plugin** does, and installs the selected version without enabling it.
|
|
50
|
-
|
|
51
|
-
|
|
54
|
+
4. The dialog then asks whether to enable the new version.
|
|
55
|
+
5. After enabling, it asks whether to restart DSH. On the desktop app the
|
|
52
56
|
`lib/restart-helper.cjs` helper relaunches the exact executable after the old
|
|
53
57
|
Host exits; under `dsh web` the dialog explains that the terminal owns the
|
|
54
58
|
process.
|
|
59
|
+
6. A management call that reports `restart-required` keeps a **Restart needed**
|
|
60
|
+
tag on the card and adds the plugin to the count beside the **Installed
|
|
61
|
+
plugins** heading, where a **Restart DSH** button confirms and performs the
|
|
62
|
+
same restart.
|
|
55
63
|
|
|
56
|
-
Power-Up
|
|
57
|
-
|
|
64
|
+
Power-Up updates itself through the same dialog. Because removing the plugin
|
|
65
|
+
unloads this panel, the Host answers first and then hands `removeBundle` →
|
|
66
|
+
`inspect` → `installBundle(enabled: true)` to the built-in manager service,
|
|
67
|
+
which keeps running after the Power-Up Host fiber is unloaded. The panel closes;
|
|
68
|
+
refresh the page when the install finishes.
|
|
58
69
|
|
|
59
70
|
## Design notes
|
|
60
71
|
|
|
@@ -78,9 +89,21 @@ unload the dialog mid-flow. Use **Add plugin** with the new package version.
|
|
|
78
89
|
no per-card slot, so the overlay injects one button into every
|
|
79
90
|
`[data-plugin-group="bundles"] [data-plugin-package]` card and re-asserts it
|
|
80
91
|
after React renders. Version listing is a plain registry `GET`; removal and
|
|
81
|
-
installation use the same `remote.pluginManager` calls as **Add plugin**.
|
|
82
|
-
|
|
83
|
-
`/dsh-powerup/api/
|
|
92
|
+
installation use the same `remote.pluginManager` calls as **Add plugin**. The
|
|
93
|
+
Host adds two loopback-only routes: `/dsh-powerup/api/restart` for the
|
|
94
|
+
process restart, and `/dsh-powerup/api/self-update` for the self-update
|
|
95
|
+
handoff.
|
|
96
|
+
- **Self-update outlives its own unload.** The Host route sends its answer, then
|
|
97
|
+
a timer starts `removeBundle` → `inspect` → `installBundle(enabled: true)`
|
|
98
|
+
through the built-in manager service. Removing the bundle unloads Power-Up's
|
|
99
|
+
own Host fiber, but the manager service and the pending promise stay in the
|
|
100
|
+
Host process, so the reinstall finishes; failures are appended to
|
|
101
|
+
`~/.dsh/powerup/self-update.log`.
|
|
102
|
+
- **Restart status lives in this browser session.** A management result that
|
|
103
|
+
says `restart-required` is written to `sessionStorage`; the card gets a
|
|
104
|
+
**Restart needed** tag and the group heading gets a count plus a restart
|
|
105
|
+
button. A real app restart clears the session, while a page refresh keeps the
|
|
106
|
+
markers.
|
|
84
107
|
- **The dialog mirrors the host modal.** The backdrop is
|
|
85
108
|
`--dsw-alias-bg-mask-1` with the shared mask blur, the surface is
|
|
86
109
|
`--dsw-alias-bg-layer-2` with `--dsw-radius-panel` and
|
package/client.js
CHANGED
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
|
|
45
45
|
const NS = 'dsh-powerup';
|
|
46
46
|
const SETTINGS_KEY = 'dsh-powerup:settings:v1';
|
|
47
|
-
const VERSION = '0.
|
|
47
|
+
const VERSION = '0.3.0';
|
|
48
48
|
const MENU_GAP = 8;
|
|
49
49
|
const MENU_WIDTH = 248;
|
|
50
50
|
|
|
@@ -154,6 +154,20 @@
|
|
|
154
154
|
'update.restartUnsupported': '当前运行方式无法自动重启:{reason}',
|
|
155
155
|
'update.restarting': 'DSH 正在重启,窗口即将关闭…',
|
|
156
156
|
'update.restartFailed': '重启请求失败:{message}',
|
|
157
|
+
'update.selfUpdateWarning': '更新 Power-Up 会在后台卸载并安装新版本;本面板会关闭,完成后刷新页面。',
|
|
158
|
+
'update.selfUpdating': 'Power-Up 正在后台更新…',
|
|
159
|
+
'update.selfRefresh': '完成后请刷新页面。',
|
|
160
|
+
'update.setDefault': '设为默认',
|
|
161
|
+
'update.defaultTag': '默认',
|
|
162
|
+
'update.defaultHint': '更新列表默认使用此安装源',
|
|
163
|
+
'restart.tag': '需重启',
|
|
164
|
+
'restart.tagHint': '该插件已更新,重启 DSH 后生效',
|
|
165
|
+
'restart.pending': '{count} 个插件待重启',
|
|
166
|
+
'restart.action': '重启 DSH',
|
|
167
|
+
'restart.title': '重启 DSH?',
|
|
168
|
+
'restart.desc': '重启会关闭并重新打开 DSH,正在运行的任务会中断。',
|
|
169
|
+
'restart.cancel': '取消',
|
|
170
|
+
'restart.confirm': '立即重启',
|
|
157
171
|
'update.doneTitle': '更新完成',
|
|
158
172
|
'update.doneInstalled': '{name} 已更新到 {version}。',
|
|
159
173
|
'update.doneDisabled': '{name} 已安装 {version},尚未启用。',
|
|
@@ -276,6 +290,20 @@
|
|
|
276
290
|
'update.restartUnsupported': 'This runtime cannot restart itself: {reason}',
|
|
277
291
|
'update.restarting': 'DSH is restarting; this window will close…',
|
|
278
292
|
'update.restartFailed': 'Restart request failed: {message}',
|
|
293
|
+
'update.selfUpdateWarning': 'Updating Power-Up removes and reinstalls it in the background; this panel closes, then refresh the page.',
|
|
294
|
+
'update.selfUpdating': 'Power-Up is updating in the background…',
|
|
295
|
+
'update.selfRefresh': 'Refresh the page when it finishes.',
|
|
296
|
+
'update.setDefault': 'Set as default',
|
|
297
|
+
'update.defaultTag': 'Default',
|
|
298
|
+
'update.defaultHint': 'The update list starts from this registry',
|
|
299
|
+
'restart.tag': 'Restart needed',
|
|
300
|
+
'restart.tagHint': 'Updated; restart DSH to apply',
|
|
301
|
+
'restart.pending': '{count} plugins need a restart',
|
|
302
|
+
'restart.action': 'Restart DSH',
|
|
303
|
+
'restart.title': 'Restart DSH?',
|
|
304
|
+
'restart.desc': 'Restarting closes and reopens DSH; running tasks are interrupted.',
|
|
305
|
+
'restart.cancel': 'Cancel',
|
|
306
|
+
'restart.confirm': 'Restart now',
|
|
279
307
|
'update.doneTitle': 'Update complete',
|
|
280
308
|
'update.doneInstalled': '{name} is now on {version}.',
|
|
281
309
|
'update.doneDisabled': '{name} {version} is installed but not enabled.',
|
|
@@ -597,6 +625,15 @@
|
|
|
597
625
|
);
|
|
598
626
|
}
|
|
599
627
|
|
|
628
|
+
function StarIcon({ size = 12 }) {
|
|
629
|
+
return h(Icon, { size },
|
|
630
|
+
h('path', {
|
|
631
|
+
d: 'm8 2.6 1.6 3.3 3.6.5-2.6 2.5.6 3.6L8 10.8l-3.2 1.7.6-3.6L2.8 6.4l3.6-.5L8 2.6Z',
|
|
632
|
+
fill: 'currentColor',
|
|
633
|
+
}),
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
|
|
600
637
|
function WarnIcon({ size = 16 }) {
|
|
601
638
|
return h(Icon, { size },
|
|
602
639
|
h('path', {
|
|
@@ -667,6 +704,15 @@
|
|
|
667
704
|
@keyframes dsh-powerup-toast-in{from{opacity:0;transform:translate(-50%,5px)}to{opacity:1;transform:translate(-50%,0)}}
|
|
668
705
|
.dsh-powerup-update-button{flex:none;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;padding:0;border:0;border-radius:var(--dsw-radius-sm,6px);background:transparent;color:var(--dsw-alias-label-caption);cursor:pointer}
|
|
669
706
|
.dsh-powerup-update-button:hover{background:var(--dsw-alias-interactive-bg-hover,var(--dsw-alias-bg-layer-2));color:var(--dsw-alias-label-secondary)}
|
|
707
|
+
.dsh-powerup-restart-tag{flex:none;display:inline-flex;align-items:center;height:18px;padding:0 6px;border:.5px solid color-mix(in srgb,var(--dsw-alias-state-warn-primary) 32%,transparent);border-radius:999px;background:var(--dsw-alias-state-warn-tertiary);color:var(--dsw-alias-state-warn-label);font-size:10px;line-height:16px;white-space:nowrap}
|
|
708
|
+
.dsh-powerup-restart-bar{display:flex;flex:none;align-items:center;gap:8px;margin-left:8px}
|
|
709
|
+
.dsh-powerup-restart-count{color:var(--dsw-alias-state-warn-label);font-size:11.5px;line-height:18px;white-space:nowrap}
|
|
710
|
+
.dsh-powerup-restart-action{height:26px;padding:0 10px;border:.5px solid color-mix(in srgb,var(--dsw-alias-state-warn-primary) 36%,transparent);border-radius:var(--dsw-radius-sm,6px);background:var(--dsw-alias-state-warn-tertiary);color:var(--dsw-alias-state-warn-label);font:inherit;font-size:11.5px;line-height:18px;cursor:pointer}
|
|
711
|
+
.dsh-powerup-restart-action:hover{background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 14%,transparent)}
|
|
712
|
+
.dsh-powerup-update-default{flex:none;display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;padding:0;border:0;border-radius:var(--dsw-radius-sm,6px);background:transparent;color:var(--dsw-alias-label-caption);cursor:pointer}
|
|
713
|
+
.dsh-powerup-update-default:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-state-warn-label)}
|
|
714
|
+
.dsh-powerup-update-default[aria-pressed=true]{color:var(--dsw-alias-state-warn-label)}
|
|
715
|
+
.dsh-powerup-update-default-tag{flex:none;padding:0 6px;border:.5px solid color-mix(in srgb,var(--dsw-alias-state-warn-primary) 32%,transparent);border-radius:999px;color:var(--dsw-alias-state-warn-label);font-size:10px;line-height:16px;white-space:nowrap}
|
|
670
716
|
.dsh-powerup-update-mask{position:fixed;inset:0;z-index:1200;display:flex;align-items:center;justify-content:center;padding:24px;background:var(--dsw-alias-bg-mask-1);backdrop-filter:var(--dsw-mask-blur,blur(3px));pointer-events:auto;animation:dsh-powerup-update-fade .16s ease-out}
|
|
671
717
|
@keyframes dsh-powerup-update-fade{from{opacity:0}to{opacity:1}}
|
|
672
718
|
.dsh-powerup-update-dialog{box-sizing:border-box;position:relative;display:flex;flex-direction:column;width:min(560px,100%);max-height:min(760px,calc(100vh - 48px));max-height:min(760px,calc(100dvh - 48px));border-radius:var(--dsw-radius-panel,14px);background:var(--dsw-alias-bg-layer-2);box-shadow:var(--dsw-elevation-prominent);color:var(--dsw-alias-label-primary);font-size:13px;line-height:20px;overflow:hidden;animation:dsh-powerup-update-rise .18s cubic-bezier(.2,.8,.3,1)}
|
|
@@ -751,7 +797,7 @@
|
|
|
751
797
|
.dsh-powerup-update-ghost{height:36px;padding:0 14px;border:0;border-radius:var(--dsw-radius-md,8px);background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:14px;line-height:22px;cursor:pointer}
|
|
752
798
|
.dsh-powerup-update-ghost:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
753
799
|
.dsh-powerup-update-ghost:active{background:var(--dsw-alias-interactive-bg-active)}
|
|
754
|
-
.dsh-powerup-update-close:focus-visible,.dsh-powerup-update-toggle:focus-visible,.dsh-powerup-update-version:focus-visible,.dsh-powerup-update-select-trigger:focus-visible,.dsh-powerup-update-option:focus-visible,.dsh-powerup-update-primary:focus-visible,.dsh-powerup-update-ghost:focus-visible,.dsh-powerup-update-button:focus-visible{outline:var(--dsw-focus-ring-width,2px) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:1px}
|
|
800
|
+
.dsh-powerup-update-close:focus-visible,.dsh-powerup-update-toggle:focus-visible,.dsh-powerup-update-version:focus-visible,.dsh-powerup-update-select-trigger:focus-visible,.dsh-powerup-update-option:focus-visible,.dsh-powerup-update-primary:focus-visible,.dsh-powerup-update-ghost:focus-visible,.dsh-powerup-update-button:focus-visible,.dsh-powerup-restart-action:focus-visible,.dsh-powerup-update-default:focus-visible{outline:var(--dsw-focus-ring-width,2px) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:1px}
|
|
755
801
|
@media (max-width:680px){.dsh-powerup-page{padding:20px 16px 42px}.dsh-powerup-actions{grid-template-columns:minmax(0,1fr)}.dsh-powerup-hero-art{width:54px;height:54px}.dsh-powerup-hero h1{font-size:19px}.dsh-powerup-card-head{flex-wrap:wrap}.dsh-powerup-switch{margin-left:auto}.dsh-powerup-update-mask{padding:12px}.dsh-powerup-update-dialog{max-height:calc(100dvh - 24px)}.dsh-powerup-update-head{padding:18px 12px 10px 18px}.dsh-powerup-update-body{padding:0 18px 4px}.dsh-powerup-update-foot{padding:14px 18px 18px}}
|
|
756
802
|
`;
|
|
757
803
|
|
|
@@ -1139,7 +1185,10 @@
|
|
|
1139
1185
|
// Plugin update: entry button on the built-in Plugins page + dialog
|
|
1140
1186
|
// ------------------------------------------------------------------
|
|
1141
1187
|
const PLUGIN_REGISTRY_MEMORY_KEY = 'dsh.plugin-manager.install-registry';
|
|
1188
|
+
const POWERUP_REGISTRY_KEY = 'dsh-powerup:update-registry:v1';
|
|
1189
|
+
const PENDING_RESTART_KEY = 'dsh-powerup:pending-restart:v1';
|
|
1142
1190
|
const UPDATE_API = '/dsh-powerup/api/restart';
|
|
1191
|
+
const SELF_UPDATE_API = '/dsh-powerup/api/self-update';
|
|
1143
1192
|
const OFFICIAL_REGISTRY = 'https://registry.npmjs.org/';
|
|
1144
1193
|
const REGISTRY_HOSTS = new Map([
|
|
1145
1194
|
['registry.npmjs.org', 'update.registryDefault'],
|
|
@@ -1188,16 +1237,75 @@
|
|
|
1188
1237
|
}
|
|
1189
1238
|
}
|
|
1190
1239
|
|
|
1240
|
+
/**
|
|
1241
|
+
* Read the registry the user starred as Power-Up's list default.
|
|
1242
|
+
* @returns {{ key?: string, value?: string | null, url?: string, label?: string, hint?: string } | null}
|
|
1243
|
+
*/
|
|
1244
|
+
function readDefaultRegistry() {
|
|
1245
|
+
try {
|
|
1246
|
+
const raw = localStorage.getItem(POWERUP_REGISTRY_KEY);
|
|
1247
|
+
if (typeof raw !== 'string' || raw === '') return null;
|
|
1248
|
+
const parsed = JSON.parse(raw);
|
|
1249
|
+
if (parsed === null || typeof parsed !== 'object') return null;
|
|
1250
|
+
return parsed;
|
|
1251
|
+
} catch {
|
|
1252
|
+
return null;
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
/**
|
|
1257
|
+
* Persist the registry the update list should start from.
|
|
1258
|
+
* @param {{ key: string, value: string | null, url: string, label: string, hint: string }} choice
|
|
1259
|
+
*/
|
|
1260
|
+
function writeDefaultRegistry(choice) {
|
|
1261
|
+
try {
|
|
1262
|
+
localStorage.setItem(POWERUP_REGISTRY_KEY, JSON.stringify({
|
|
1263
|
+
key: choice.key,
|
|
1264
|
+
value: choice.value,
|
|
1265
|
+
url: choice.url,
|
|
1266
|
+
label: choice.label,
|
|
1267
|
+
hint: choice.hint,
|
|
1268
|
+
}));
|
|
1269
|
+
} catch {
|
|
1270
|
+
/* A full or private store keeps the in-memory selection. */
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
/** Pending-restart records for this browser session (cleared by a real restart). */
|
|
1275
|
+
function readPendingRestarts() {
|
|
1276
|
+
try {
|
|
1277
|
+
const raw = sessionStorage.getItem(PENDING_RESTART_KEY);
|
|
1278
|
+
if (typeof raw !== 'string' || raw === '') return [];
|
|
1279
|
+
const parsed = JSON.parse(raw);
|
|
1280
|
+
return Array.isArray(parsed) ? parsed.filter((entry) => entry !== null && typeof entry === 'object' && typeof entry.name === 'string') : [];
|
|
1281
|
+
} catch {
|
|
1282
|
+
return [];
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
/**
|
|
1287
|
+
* @param {Array<{ name: string, version?: string, reason?: string, at?: number }>} entries
|
|
1288
|
+
*/
|
|
1289
|
+
function writePendingRestarts(entries) {
|
|
1290
|
+
try {
|
|
1291
|
+
sessionStorage.setItem(PENDING_RESTART_KEY, JSON.stringify(entries));
|
|
1292
|
+
} catch {
|
|
1293
|
+
/* The badge is a hint; failing to persist it must not break updates. */
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1191
1297
|
/**
|
|
1192
1298
|
* The registry choices the update dialog offers: the Host's configured
|
|
1193
1299
|
* default (with the URL pnpm actually names), each configured fallback,
|
|
1194
|
-
*
|
|
1300
|
+
* the built-in Add dialog's remembered custom address, and Power-Up's
|
|
1301
|
+
* own starred default.
|
|
1195
1302
|
* @param {{ registry: string | null, fallbackRegistries: string[], resolved: string | null } | null} registries
|
|
1196
1303
|
* @param {object | null} remembered
|
|
1304
|
+
* @param {object | null} powerupDefault
|
|
1197
1305
|
* @param {(key: string) => string} t
|
|
1198
1306
|
* @returns {Array<{ key: string, label: string, hint: string, value: string | null, url: string }>}
|
|
1199
1307
|
*/
|
|
1200
|
-
function buildRegistryChoices(registries, remembered, t) {
|
|
1308
|
+
function buildRegistryChoices(registries, remembered, powerupDefault, t) {
|
|
1201
1309
|
const choices = [];
|
|
1202
1310
|
const seen = new Set();
|
|
1203
1311
|
const add = (choice) => {
|
|
@@ -1246,6 +1354,18 @@
|
|
|
1246
1354
|
url: normalizeRegistry(rememberedOffered),
|
|
1247
1355
|
});
|
|
1248
1356
|
}
|
|
1357
|
+
if (powerupDefault !== null && typeof powerupDefault === 'object') {
|
|
1358
|
+
const value = powerupDefault.value === undefined ? null : powerupDefault.value;
|
|
1359
|
+
const url = powerupDefault.url ?? value ?? OFFICIAL_REGISTRY;
|
|
1360
|
+
add({
|
|
1361
|
+
label: typeof powerupDefault.label === 'string'
|
|
1362
|
+
? powerupDefault.label
|
|
1363
|
+
: (value === null ? t('update.registryDefault') : t('update.registryCustom')),
|
|
1364
|
+
hint: typeof powerupDefault.hint === 'string' ? powerupDefault.hint : registryHost(url),
|
|
1365
|
+
value,
|
|
1366
|
+
url: normalizeRegistry(url),
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1249
1369
|
if (choices.length === 0) {
|
|
1250
1370
|
add({
|
|
1251
1371
|
label: t('update.registryDefault'),
|
|
@@ -1258,12 +1378,35 @@
|
|
|
1258
1378
|
}
|
|
1259
1379
|
|
|
1260
1380
|
/**
|
|
1261
|
-
*
|
|
1381
|
+
* Find the offered registry matching a persisted Power-Up default.
|
|
1382
|
+
* @param {Array<{ key: string, value: string | null, url: string }>} choices
|
|
1383
|
+
* @param {{ key?: string, value?: string | null, url?: string } | null} stored
|
|
1384
|
+
* @returns {object | undefined}
|
|
1385
|
+
*/
|
|
1386
|
+
function matchStoredChoice(choices, stored) {
|
|
1387
|
+
if (stored === null || typeof stored !== 'object') return undefined;
|
|
1388
|
+
if (typeof stored.key === 'string') {
|
|
1389
|
+
const byKey = choices.find((choice) => choice.key === stored.key);
|
|
1390
|
+
if (byKey !== undefined) return byKey;
|
|
1391
|
+
}
|
|
1392
|
+
const value = stored.value === undefined ? null : stored.value;
|
|
1393
|
+
if (value === null) return choices.find((choice) => choice.value === null);
|
|
1394
|
+
const wanted = normalizeRegistry(value ?? stored.url ?? OFFICIAL_REGISTRY);
|
|
1395
|
+
return choices.find((choice) => choice.value !== null && normalizeRegistry(choice.value) === wanted)
|
|
1396
|
+
?? choices.find((choice) => normalizeRegistry(choice.url) === wanted);
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
/**
|
|
1400
|
+
* Choose the registry the update list starts from: Power-Up's starred
|
|
1401
|
+
* default, then what the Add dialog remembered, then the configured one.
|
|
1262
1402
|
* @param {Array<{ key: string, value: string | null, url: string }>} choices
|
|
1263
1403
|
* @param {object | null} remembered
|
|
1404
|
+
* @param {object | null} powerupDefault
|
|
1264
1405
|
* @returns {object}
|
|
1265
1406
|
*/
|
|
1266
|
-
function pickRegistryChoice(choices, remembered) {
|
|
1407
|
+
function pickRegistryChoice(choices, remembered, powerupDefault) {
|
|
1408
|
+
const starred = matchStoredChoice(choices, powerupDefault);
|
|
1409
|
+
if (starred !== undefined) return starred;
|
|
1267
1410
|
if (remembered !== null && typeof remembered === 'object') {
|
|
1268
1411
|
if (remembered.kind === 'offered') {
|
|
1269
1412
|
const wanted = remembered.registry === null ? null : normalizeRegistry(remembered.registry);
|
|
@@ -1462,20 +1605,23 @@
|
|
|
1462
1605
|
|
|
1463
1606
|
/** Tooltip/accessible label for one injected update button. */
|
|
1464
1607
|
function updateButtonLabel(name, t) {
|
|
1465
|
-
return
|
|
1608
|
+
return `${t('update.button')} · ${name}`;
|
|
1466
1609
|
}
|
|
1467
1610
|
|
|
1468
1611
|
/**
|
|
1469
1612
|
* Ensure every third-party card in the built-in "Installed plugins"
|
|
1470
|
-
* group carries one update button
|
|
1471
|
-
*
|
|
1613
|
+
* group carries one update button and, when its update waits for a
|
|
1614
|
+
* restart, a small restart tag. Idempotent, so React re-renders and the
|
|
1615
|
+
* observer can call it repeatedly.
|
|
1472
1616
|
* @param {Document} doc
|
|
1473
1617
|
* @param {(name: string) => void} open
|
|
1618
|
+
* @param {string[]} pendingNames
|
|
1474
1619
|
* @param {(key: string, params?: object) => string} t
|
|
1475
1620
|
* @param {Set<Element>} buttons
|
|
1476
1621
|
*/
|
|
1477
|
-
function ensureUpdateButtons(doc, open, t, buttons) {
|
|
1622
|
+
function ensureUpdateButtons(doc, open, pendingNames, t, buttons) {
|
|
1478
1623
|
if (doc === null || doc === undefined || typeof doc.querySelectorAll !== 'function') return;
|
|
1624
|
+
const pending = new Set(Array.isArray(pendingNames) ? pendingNames : []);
|
|
1479
1625
|
const cards = doc.querySelectorAll('[data-plugin-group="bundles"] [data-plugin-package]');
|
|
1480
1626
|
for (const card of cards) {
|
|
1481
1627
|
const name = card.getAttribute('data-plugin-package');
|
|
@@ -1501,10 +1647,78 @@
|
|
|
1501
1647
|
const label = updateButtonLabel(name, t);
|
|
1502
1648
|
if (button.title !== label) button.title = label;
|
|
1503
1649
|
if (button.getAttribute('aria-label') !== label) button.setAttribute('aria-label', label);
|
|
1650
|
+
|
|
1651
|
+
let tag = card.querySelector('[data-dsh-powerup-restart-tag]');
|
|
1652
|
+
if (pending.has(name)) {
|
|
1653
|
+
if (tag === null || tag === undefined) {
|
|
1654
|
+
const head = card.firstElementChild;
|
|
1655
|
+
const main = head === null || head === undefined ? null : head.children?.[1];
|
|
1656
|
+
const titleRow = main === null || main === undefined ? null : main.firstElementChild;
|
|
1657
|
+
if (titleRow !== null && titleRow !== undefined) {
|
|
1658
|
+
tag = doc.createElement('span');
|
|
1659
|
+
tag.className = 'dsh-powerup-restart-tag';
|
|
1660
|
+
tag.setAttribute('data-dsh-powerup-restart-tag', '');
|
|
1661
|
+
titleRow.appendChild(tag);
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
if (tag !== null && tag !== undefined) {
|
|
1665
|
+
const text = t('restart.tag');
|
|
1666
|
+
if (tag.textContent !== text) tag.textContent = text;
|
|
1667
|
+
if (tag.title !== t('restart.tagHint')) tag.title = t('restart.tagHint');
|
|
1668
|
+
}
|
|
1669
|
+
} else if (tag !== null && tag !== undefined) {
|
|
1670
|
+
tag.remove();
|
|
1671
|
+
}
|
|
1504
1672
|
}
|
|
1505
1673
|
for (const button of [...buttons]) if (button.isConnected !== true) buttons.delete(button);
|
|
1506
1674
|
}
|
|
1507
1675
|
|
|
1676
|
+
/**
|
|
1677
|
+
* Put "N plugins need a restart" and a restart button beside the
|
|
1678
|
+
* Installed plugins heading. The group head owns the h3; the injected bar
|
|
1679
|
+
* sits at the end of that row and is removed when nothing is pending.
|
|
1680
|
+
* @param {Document} doc
|
|
1681
|
+
* @param {number} count
|
|
1682
|
+
* @param {() => void} onRestart
|
|
1683
|
+
* @param {(key: string, params?: object) => string} t
|
|
1684
|
+
*/
|
|
1685
|
+
function ensureRestartBar(doc, count, onRestart, t) {
|
|
1686
|
+
if (doc === null || doc === undefined || typeof doc.querySelector !== 'function') return;
|
|
1687
|
+
const group = doc.querySelector('[data-plugin-group="bundles"]');
|
|
1688
|
+
if (group === null || group === undefined || count <= 0) {
|
|
1689
|
+
const stale = group?.querySelector?.('[data-dsh-powerup-restart-bar]');
|
|
1690
|
+
stale?.remove?.();
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1693
|
+
const head = group.firstElementChild;
|
|
1694
|
+
if (head === null || head === undefined) return;
|
|
1695
|
+
let bar = group.querySelector('[data-dsh-powerup-restart-bar]');
|
|
1696
|
+
if (bar === null || bar === undefined) {
|
|
1697
|
+
bar = doc.createElement('div');
|
|
1698
|
+
bar.className = 'dsh-powerup-restart-bar';
|
|
1699
|
+
bar.setAttribute('data-dsh-powerup-restart-bar', '');
|
|
1700
|
+
const label = doc.createElement('span');
|
|
1701
|
+
label.className = 'dsh-powerup-restart-count';
|
|
1702
|
+
const button = doc.createElement('button');
|
|
1703
|
+
button.type = 'button';
|
|
1704
|
+
button.className = 'dsh-powerup-restart-action';
|
|
1705
|
+
button.addEventListener('click', (event) => {
|
|
1706
|
+
event.preventDefault();
|
|
1707
|
+
event.stopPropagation();
|
|
1708
|
+
onRestart();
|
|
1709
|
+
});
|
|
1710
|
+
bar.appendChild(label);
|
|
1711
|
+
bar.appendChild(button);
|
|
1712
|
+
head.appendChild(bar);
|
|
1713
|
+
}
|
|
1714
|
+
const label = bar.firstElementChild;
|
|
1715
|
+
const button = bar.lastElementChild;
|
|
1716
|
+
const text = t('restart.pending', { count });
|
|
1717
|
+
if (label !== null && label !== undefined && label.textContent !== text) label.textContent = text;
|
|
1718
|
+
const action = t('restart.action');
|
|
1719
|
+
if (button !== null && button !== undefined && button.textContent !== action) button.textContent = action;
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1508
1722
|
/**
|
|
1509
1723
|
* Place a floating panel under an anchor, flipping above when the space
|
|
1510
1724
|
* below is short, and clamp it to the viewport.
|
|
@@ -1542,6 +1756,11 @@
|
|
|
1542
1756
|
function PluginUpdateOverlay({ manager, remote, ctx }) {
|
|
1543
1757
|
const [view, setView] = useState(null);
|
|
1544
1758
|
const [localeTick, forceLocale] = useState(0);
|
|
1759
|
+
const [pending, setPending] = useState(() => readPendingRestarts());
|
|
1760
|
+
const [restartAsk, setRestartAsk] = useState(false);
|
|
1761
|
+
const [restartCapability, setRestartCapability] = useState(null);
|
|
1762
|
+
const [restartBusy, setRestartBusy] = useState(false);
|
|
1763
|
+
const [restartError, setRestartError] = useState(null);
|
|
1545
1764
|
const tokenRef = useRef(null);
|
|
1546
1765
|
const abortRef = useRef(null);
|
|
1547
1766
|
const buttonsRef = useRef(new Set());
|
|
@@ -1551,6 +1770,7 @@
|
|
|
1551
1770
|
const registryRef = useRef(null);
|
|
1552
1771
|
const registryPanelRef = useRef(null);
|
|
1553
1772
|
const versionSeq = useRef(0);
|
|
1773
|
+
const openRestartRef = useRef(() => {});
|
|
1554
1774
|
const tRef = useRef((key, params) => translateCurrent(key, params));
|
|
1555
1775
|
tRef.current = (key, params) => translateCurrent(key, params);
|
|
1556
1776
|
|
|
@@ -1570,7 +1790,7 @@
|
|
|
1570
1790
|
|
|
1571
1791
|
const close = useCallback(() => {
|
|
1572
1792
|
const current = view;
|
|
1573
|
-
if (current !== null && (current.phase === 'working' || current.phase === 'restarting' || current.enabling === true)) return;
|
|
1793
|
+
if (current !== null && (current.phase === 'working' || current.phase === 'self-updating' || current.phase === 'restarting' || current.enabling === true)) return;
|
|
1574
1794
|
tokenRef.current = null;
|
|
1575
1795
|
try {
|
|
1576
1796
|
abortRef.current?.abort?.();
|
|
@@ -1592,6 +1812,25 @@
|
|
|
1592
1812
|
});
|
|
1593
1813
|
}, []);
|
|
1594
1814
|
|
|
1815
|
+
/** Remember that one updated plugin waits for a whole-app restart. */
|
|
1816
|
+
const recordPending = useCallback((entry) => {
|
|
1817
|
+
setPending((current) => {
|
|
1818
|
+
const next = current.filter((item) => item.name !== entry.name);
|
|
1819
|
+
next.push({ ...entry, at: Date.now() });
|
|
1820
|
+
writePendingRestarts(next);
|
|
1821
|
+
return next;
|
|
1822
|
+
});
|
|
1823
|
+
}, []);
|
|
1824
|
+
|
|
1825
|
+
/** Drop a pending marker when its update never started. */
|
|
1826
|
+
const removePending = useCallback((name) => {
|
|
1827
|
+
setPending((current) => {
|
|
1828
|
+
const next = current.filter((item) => item.name !== name);
|
|
1829
|
+
writePendingRestarts(next);
|
|
1830
|
+
return next;
|
|
1831
|
+
});
|
|
1832
|
+
}, []);
|
|
1833
|
+
|
|
1595
1834
|
const loadVersions = useCallback(async (token, name, choice, choices, resetVersion) => {
|
|
1596
1835
|
const t = tRef.current;
|
|
1597
1836
|
const seq = versionSeq.current + 1;
|
|
@@ -1640,11 +1879,13 @@
|
|
|
1640
1879
|
setView({
|
|
1641
1880
|
name,
|
|
1642
1881
|
title: name,
|
|
1882
|
+
self: name === MODULE_ID,
|
|
1643
1883
|
currentVersion: null,
|
|
1644
1884
|
phase: 'loading',
|
|
1645
1885
|
step: null,
|
|
1646
1886
|
choices: [],
|
|
1647
1887
|
registryKey: '',
|
|
1888
|
+
defaultRegistryKey: null,
|
|
1648
1889
|
versions: [],
|
|
1649
1890
|
versionsState: 'loading',
|
|
1650
1891
|
versionsError: null,
|
|
@@ -1660,10 +1901,6 @@
|
|
|
1660
1901
|
enabling: false,
|
|
1661
1902
|
restarting: false,
|
|
1662
1903
|
});
|
|
1663
|
-
if (name === MODULE_ID) {
|
|
1664
|
-
patch(token, { phase: 'self' });
|
|
1665
|
-
return;
|
|
1666
|
-
}
|
|
1667
1904
|
try {
|
|
1668
1905
|
const [bundles, registries] = await Promise.all([manager.listBundles(), manager.registries()]);
|
|
1669
1906
|
if (tokenRef.current !== token) return;
|
|
@@ -1680,8 +1917,10 @@
|
|
|
1680
1917
|
return;
|
|
1681
1918
|
}
|
|
1682
1919
|
const remembered = readRememberedRegistry();
|
|
1683
|
-
const
|
|
1684
|
-
const
|
|
1920
|
+
const powerupDefault = readDefaultRegistry();
|
|
1921
|
+
const choices = buildRegistryChoices(registries.ok ? registries.value : null, remembered, powerupDefault, t);
|
|
1922
|
+
const choice = pickRegistryChoice(choices, remembered, powerupDefault);
|
|
1923
|
+
const starred = matchStoredChoice(choices, powerupDefault);
|
|
1685
1924
|
patch(token, {
|
|
1686
1925
|
title: packageDisplayTitle(pkg),
|
|
1687
1926
|
currentVersion: pkg.version ?? null,
|
|
@@ -1689,6 +1928,7 @@
|
|
|
1689
1928
|
phase: 'choose',
|
|
1690
1929
|
choices,
|
|
1691
1930
|
registryKey: choice.key,
|
|
1931
|
+
defaultRegistryKey: starred === undefined ? null : starred.key,
|
|
1692
1932
|
versionsState: 'loading',
|
|
1693
1933
|
});
|
|
1694
1934
|
await loadVersions(token, name, choice, choices, false);
|
|
@@ -1710,7 +1950,9 @@
|
|
|
1710
1950
|
const ensure = () => {
|
|
1711
1951
|
scheduled = false;
|
|
1712
1952
|
if (stopped) return;
|
|
1713
|
-
|
|
1953
|
+
const names = pending.map((entry) => entry.name);
|
|
1954
|
+
ensureUpdateButtons(document, (name) => openRef.current(name), names, tRef.current, buttons);
|
|
1955
|
+
ensureRestartBar(document, pending.length, () => openRestartRef.current(), tRef.current);
|
|
1714
1956
|
};
|
|
1715
1957
|
const schedule = () => {
|
|
1716
1958
|
if (scheduled || stopped) return;
|
|
@@ -1733,7 +1975,7 @@
|
|
|
1733
1975
|
}
|
|
1734
1976
|
buttons.clear();
|
|
1735
1977
|
};
|
|
1736
|
-
}, [localeTick]);
|
|
1978
|
+
}, [localeTick, pending]);
|
|
1737
1979
|
|
|
1738
1980
|
// Keep the selected version input focused when the chooser opens.
|
|
1739
1981
|
useEffect(() => {
|
|
@@ -1824,6 +2066,19 @@
|
|
|
1824
2066
|
};
|
|
1825
2067
|
}, [remote]);
|
|
1826
2068
|
|
|
2069
|
+
// Escape dismisses the list-header restart confirmation unless a
|
|
2070
|
+
// restart request is already in flight.
|
|
2071
|
+
useEffect(() => {
|
|
2072
|
+
if (!restartAsk) return undefined;
|
|
2073
|
+
const onKeyDown = (event) => {
|
|
2074
|
+
if (event.key !== 'Escape' || restartBusy) return;
|
|
2075
|
+
event.stopPropagation();
|
|
2076
|
+
setRestartAsk(false);
|
|
2077
|
+
};
|
|
2078
|
+
document.addEventListener('keydown', onKeyDown, true);
|
|
2079
|
+
return () => document.removeEventListener('keydown', onKeyDown, true);
|
|
2080
|
+
}, [restartAsk, restartBusy]);
|
|
2081
|
+
|
|
1827
2082
|
const chooseVersion = (version) => {
|
|
1828
2083
|
if (tokenRef.current === null) return;
|
|
1829
2084
|
setView((current) => current === null ? current : { ...current, version, listOpen: false, listPopover: null, formError: null });
|
|
@@ -1840,6 +2095,19 @@
|
|
|
1840
2095
|
loadVersions(token, current.name, choice, current.choices, true);
|
|
1841
2096
|
};
|
|
1842
2097
|
|
|
2098
|
+
/** Star one registry as Power-Up's default and select it for this update. */
|
|
2099
|
+
const setDefaultRegistry = (choice) => {
|
|
2100
|
+
if (choice === null || choice === undefined) return;
|
|
2101
|
+
const current = view;
|
|
2102
|
+
writeDefaultRegistry(choice);
|
|
2103
|
+
setView((state) => state === null ? state : {
|
|
2104
|
+
...state,
|
|
2105
|
+
defaultRegistryKey: choice.key,
|
|
2106
|
+
registryKey: choice.key,
|
|
2107
|
+
});
|
|
2108
|
+
if (current !== null) loadVersions(tokenRef.current, current.name, choice, current.choices, true);
|
|
2109
|
+
};
|
|
2110
|
+
|
|
1843
2111
|
/** Open or close the floating version list under the input row. */
|
|
1844
2112
|
const toggleVersionList = () => {
|
|
1845
2113
|
const current = view;
|
|
@@ -1892,7 +2160,29 @@
|
|
|
1892
2160
|
const registry = registryValue(choice);
|
|
1893
2161
|
const spec = `${current.name}@${version}`;
|
|
1894
2162
|
const requestId = newRequestId();
|
|
1895
|
-
patch(token, { phase: 'working', step: 'removing', error: null, formError: null, log: '', logLabel: spec });
|
|
2163
|
+
patch(token, { phase: 'working', step: current.self === true ? 'installing' : 'removing', error: null, formError: null, log: '', logLabel: spec });
|
|
2164
|
+
if (current.self === true) {
|
|
2165
|
+
// Power-Up cannot reinstall itself from its own client: removing
|
|
2166
|
+
// the bundle unloads this dialog. The Host route receives the
|
|
2167
|
+
// request, answers, then hands remove+inspect+install to the
|
|
2168
|
+
// built-in manager service, which keeps running in the Host
|
|
2169
|
+
// process after this plugin unloads.
|
|
2170
|
+
recordPending({ name: MODULE_ID, version, reason: 'self-update' });
|
|
2171
|
+
try {
|
|
2172
|
+
const response = await fetch(SELF_UPDATE_API, {
|
|
2173
|
+
method: 'POST',
|
|
2174
|
+
headers: { 'content-type': 'application/json' },
|
|
2175
|
+
body: JSON.stringify({ version, registry }),
|
|
2176
|
+
});
|
|
2177
|
+
const data = await response.json().catch(() => null);
|
|
2178
|
+
if (!response.ok || data?.ok !== true) throw new Error(data?.error ?? `HTTP ${response.status}`);
|
|
2179
|
+
patch(token, { phase: 'self-updating', step: null });
|
|
2180
|
+
} catch (error) {
|
|
2181
|
+
removePending(MODULE_ID);
|
|
2182
|
+
patch(token, { phase: 'error', step: null, error: errorText(error) || t('update.unknown') });
|
|
2183
|
+
}
|
|
2184
|
+
return;
|
|
2185
|
+
}
|
|
1896
2186
|
try {
|
|
1897
2187
|
// The update flow removes the old version first: the Host's
|
|
1898
2188
|
// inspection refuses a package that is still installed, so the
|
|
@@ -1915,11 +2205,19 @@
|
|
|
1915
2205
|
const value = installed.value;
|
|
1916
2206
|
if (value.error !== undefined || value.application === 'failed') throw new Error(operationReason(value, t));
|
|
1917
2207
|
if (value.application === 'cancelled') throw new Error(t('update.cancelled'));
|
|
2208
|
+
const restartRequired = value.application === 'restart-required';
|
|
2209
|
+
if (restartRequired) {
|
|
2210
|
+
recordPending({
|
|
2211
|
+
name: current.name,
|
|
2212
|
+
version: value.version ?? version,
|
|
2213
|
+
reason: value.restartReason ?? '',
|
|
2214
|
+
});
|
|
2215
|
+
}
|
|
1918
2216
|
patch(token, {
|
|
1919
2217
|
phase: 'enable',
|
|
1920
2218
|
step: null,
|
|
1921
2219
|
installedVersion: value.version ?? version,
|
|
1922
|
-
restartRequired
|
|
2220
|
+
restartRequired,
|
|
1923
2221
|
enabling: false,
|
|
1924
2222
|
});
|
|
1925
2223
|
} catch (error) {
|
|
@@ -1941,6 +2239,35 @@
|
|
|
1941
2239
|
}
|
|
1942
2240
|
};
|
|
1943
2241
|
|
|
2242
|
+
/** Open the list-header restart confirmation and read the capability. */
|
|
2243
|
+
const openRestart = () => {
|
|
2244
|
+
setRestartAsk(true);
|
|
2245
|
+
setRestartCapability(null);
|
|
2246
|
+
setRestartError(null);
|
|
2247
|
+
setRestartBusy(false);
|
|
2248
|
+
void readRestartCapability().then((capability) => setRestartCapability(capability));
|
|
2249
|
+
};
|
|
2250
|
+
openRestartRef.current = openRestart;
|
|
2251
|
+
|
|
2252
|
+
/** Confirm the list-header restart. */
|
|
2253
|
+
const confirmRestart = async () => {
|
|
2254
|
+
if (restartBusy) return;
|
|
2255
|
+
setRestartBusy(true);
|
|
2256
|
+
setRestartError(null);
|
|
2257
|
+
try {
|
|
2258
|
+
const response = await fetch(UPDATE_API, {
|
|
2259
|
+
method: 'POST',
|
|
2260
|
+
headers: { 'content-type': 'application/json' },
|
|
2261
|
+
body: JSON.stringify({ action: 'restart' }),
|
|
2262
|
+
});
|
|
2263
|
+
const data = await response.json().catch(() => null);
|
|
2264
|
+
if (!response.ok || data?.ok !== true) throw new Error(data?.error ?? `HTTP ${response.status}`);
|
|
2265
|
+
} catch (error) {
|
|
2266
|
+
setRestartBusy(false);
|
|
2267
|
+
setRestartError(errorText(error) || tRef.current('update.unknown'));
|
|
2268
|
+
}
|
|
2269
|
+
};
|
|
2270
|
+
|
|
1944
2271
|
const enableNow = async () => {
|
|
1945
2272
|
const token = tokenRef.current;
|
|
1946
2273
|
const current = view;
|
|
@@ -1952,6 +2279,13 @@
|
|
|
1952
2279
|
if (tokenRef.current !== token) return;
|
|
1953
2280
|
if (!result.ok) throw new Error(result.error?.message ?? t('update.unknown'));
|
|
1954
2281
|
if (result.value.error !== undefined) throw new Error(operationReason(result.value, t));
|
|
2282
|
+
if (result.value.application === 'restart-required' || current.restartRequired === true) {
|
|
2283
|
+
recordPending({
|
|
2284
|
+
name: current.name,
|
|
2285
|
+
version: current.installedVersion ?? '',
|
|
2286
|
+
reason: result.value.restartReason ?? '',
|
|
2287
|
+
});
|
|
2288
|
+
}
|
|
1955
2289
|
const restart = await readRestartCapability();
|
|
1956
2290
|
patch(token, { phase: 'restart', enabling: false, restart });
|
|
1957
2291
|
} catch (error) {
|
|
@@ -1990,9 +2324,64 @@
|
|
|
1990
2324
|
}
|
|
1991
2325
|
};
|
|
1992
2326
|
|
|
1993
|
-
if (view === null) return null;
|
|
1994
2327
|
const t = (key, params) => translateCurrent(key, params);
|
|
1995
|
-
|
|
2328
|
+
|
|
2329
|
+
/** One tinted notice: the success/warn/error states share this chrome. */
|
|
2330
|
+
const notice = (tone, icon, title, message) => h('div', { className: `dsh-powerup-update-notice ${tone}` },
|
|
2331
|
+
icon,
|
|
2332
|
+
h('div', { className: 'dsh-powerup-update-notice-copy' },
|
|
2333
|
+
title === null || title === undefined ? null : h('span', { className: 'dsh-powerup-update-notice-title' }, title),
|
|
2334
|
+
message === null || message === undefined ? null : h('span', null, message),
|
|
2335
|
+
),
|
|
2336
|
+
);
|
|
2337
|
+
|
|
2338
|
+
const restartPrompt = restartAsk ? h('div', {
|
|
2339
|
+
className: 'dsh-powerup-update-mask',
|
|
2340
|
+
onMouseDown: (event) => {
|
|
2341
|
+
if (event.target === event.currentTarget && !restartBusy) setRestartAsk(false);
|
|
2342
|
+
},
|
|
2343
|
+
},
|
|
2344
|
+
h('div', {
|
|
2345
|
+
className: 'dsh-powerup-update-dialog',
|
|
2346
|
+
style: { width: 'min(420px,100%)' },
|
|
2347
|
+
role: 'dialog',
|
|
2348
|
+
'aria-modal': 'true',
|
|
2349
|
+
'aria-labelledby': 'dsh-powerup-restart-title',
|
|
2350
|
+
},
|
|
2351
|
+
h('div', { className: 'dsh-powerup-update-head' },
|
|
2352
|
+
h('div', { className: 'dsh-powerup-update-heading' },
|
|
2353
|
+
h('h2', { id: 'dsh-powerup-restart-title' }, t('restart.title')),
|
|
2354
|
+
h('p', null, t('restart.pending', { count: pending.length })),
|
|
2355
|
+
),
|
|
2356
|
+
restartBusy ? null : h('button', { type: 'button', className: 'dsh-powerup-update-close', 'aria-label': t('update.close'), onClick: () => setRestartAsk(false) }, '×'),
|
|
2357
|
+
),
|
|
2358
|
+
h('div', { className: 'dsh-powerup-update-body' },
|
|
2359
|
+
h('p', { className: 'dsh-powerup-update-note' }, t('restart.desc')),
|
|
2360
|
+
restartCapability !== null && restartCapability.supported !== true
|
|
2361
|
+
? notice('warn', h(WarnIcon, { size: 14 }), null, t('update.restartUnsupported', { reason: restartCapability.reason ?? '' }))
|
|
2362
|
+
: null,
|
|
2363
|
+
restartError === null ? null : notice('error', h(WarnIcon, { size: 14 }), null, restartError),
|
|
2364
|
+
restartBusy
|
|
2365
|
+
? h('div', { className: 'dsh-powerup-update-status' },
|
|
2366
|
+
h('span', { className: 'dsh-powerup-update-spinner', 'aria-hidden': 'true' }),
|
|
2367
|
+
t('update.restarting'),
|
|
2368
|
+
)
|
|
2369
|
+
: null,
|
|
2370
|
+
),
|
|
2371
|
+
h('div', { className: 'dsh-powerup-update-foot' },
|
|
2372
|
+
h('button', { type: 'button', className: 'dsh-powerup-update-ghost', disabled: restartBusy, onClick: () => setRestartAsk(false) }, t('restart.cancel')),
|
|
2373
|
+
h('button', {
|
|
2374
|
+
type: 'button',
|
|
2375
|
+
className: 'dsh-powerup-update-primary',
|
|
2376
|
+
disabled: restartBusy || restartCapability?.supported !== true,
|
|
2377
|
+
onClick: confirmRestart,
|
|
2378
|
+
}, restartBusy ? t('update.restarting') : t('restart.confirm')),
|
|
2379
|
+
),
|
|
2380
|
+
),
|
|
2381
|
+
) : null;
|
|
2382
|
+
|
|
2383
|
+
if (view === null) return restartPrompt;
|
|
2384
|
+
const busy = view.phase === 'working' || view.phase === 'self-updating' || view.phase === 'restarting' || view.enabling === true;
|
|
1996
2385
|
const canClose = !busy;
|
|
1997
2386
|
const title = view.title === undefined || view.title === '' ? view.name : view.title;
|
|
1998
2387
|
const showVersions = view.phase === 'choose' && view.listOpen === true;
|
|
@@ -2023,21 +2412,12 @@
|
|
|
2023
2412
|
supported ? h('button', { key: 'now', type: 'button', className: 'dsh-powerup-update-primary', disabled: view.restarting === true, onClick: restartNow }, view.restarting === true ? t('update.restarting') : t('update.restartNow')) : null,
|
|
2024
2413
|
].filter(Boolean);
|
|
2025
2414
|
}
|
|
2026
|
-
if (view.phase === 'restarting' || view.phase === 'working' || view.phase === 'loading') return [];
|
|
2415
|
+
if (view.phase === 'restarting' || view.phase === 'working' || view.phase === 'self-updating' || view.phase === 'loading') return [];
|
|
2027
2416
|
return [
|
|
2028
2417
|
h('button', { key: 'done', type: 'button', className: 'dsh-powerup-update-primary', onClick: close }, view.phase === 'done' ? t('update.doneButton') : t('update.close')),
|
|
2029
2418
|
];
|
|
2030
2419
|
})();
|
|
2031
2420
|
|
|
2032
|
-
/** One tinted notice: the success/warn/error states share this chrome. */
|
|
2033
|
-
const notice = (tone, icon, title, message) => h('div', { className: `dsh-powerup-update-notice ${tone}` },
|
|
2034
|
-
icon,
|
|
2035
|
-
h('div', { className: 'dsh-powerup-update-notice-copy' },
|
|
2036
|
-
title === null || title === undefined ? null : h('span', { className: 'dsh-powerup-update-notice-title' }, title),
|
|
2037
|
-
message === null || message === undefined ? null : h('span', null, message),
|
|
2038
|
-
),
|
|
2039
|
-
);
|
|
2040
|
-
|
|
2041
2421
|
const currentChoice = view.choices.find((item) => item.key === view.registryKey) ?? view.choices[0];
|
|
2042
2422
|
|
|
2043
2423
|
/** Rows of the floating version list, including its loading/error states. */
|
|
@@ -2102,8 +2482,15 @@
|
|
|
2102
2482
|
t('update.loading'),
|
|
2103
2483
|
);
|
|
2104
2484
|
}
|
|
2105
|
-
if (view.phase === 'self') {
|
|
2106
|
-
return
|
|
2485
|
+
if (view.phase === 'self-updating') {
|
|
2486
|
+
return [
|
|
2487
|
+
h('div', { key: 'status', className: 'dsh-powerup-update-status' },
|
|
2488
|
+
h('span', { className: 'dsh-powerup-update-spinner', 'aria-hidden': 'true' }),
|
|
2489
|
+
t('update.selfUpdating'),
|
|
2490
|
+
),
|
|
2491
|
+
h('p', { key: 'note', className: 'dsh-powerup-update-note' }, t('update.selfRefresh')),
|
|
2492
|
+
h('span', { key: 'name', className: 'dsh-powerup-update-name' }, view.name),
|
|
2493
|
+
];
|
|
2107
2494
|
}
|
|
2108
2495
|
if (view.phase === 'blocked') {
|
|
2109
2496
|
return notice('error', h(WarnIcon, { size: 15 }), t('update.unsupportedTitle'), view.error ?? t('update.notRemovable'));
|
|
@@ -2118,6 +2505,9 @@
|
|
|
2118
2505
|
),
|
|
2119
2506
|
h('span', { className: 'dsh-powerup-update-name' }, view.name),
|
|
2120
2507
|
),
|
|
2508
|
+
view.self === true
|
|
2509
|
+
? notice('warn', h(WarnIcon, { size: 14 }), null, t('update.selfUpdateWarning'))
|
|
2510
|
+
: null,
|
|
2121
2511
|
h('div', { key: 'version', className: 'dsh-powerup-update-field' },
|
|
2122
2512
|
h('label', { className: 'dsh-powerup-update-label', htmlFor: 'dsh-powerup-update-version' }, t('update.versionLabel')),
|
|
2123
2513
|
h('div', { className: 'dsh-powerup-update-row', ref: versionRef },
|
|
@@ -2289,20 +2679,42 @@
|
|
|
2289
2679
|
role: 'listbox',
|
|
2290
2680
|
'aria-label': t('update.registry'),
|
|
2291
2681
|
style: view.registryPopover ?? fallbackPopover,
|
|
2292
|
-
}, view.choices.map((choice) =>
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2682
|
+
}, view.choices.map((choice) => {
|
|
2683
|
+
const isDefault = choice.key === view.defaultRegistryKey;
|
|
2684
|
+
const isChosen = choice.key === view.registryKey;
|
|
2685
|
+
return h('div', {
|
|
2686
|
+
key: choice.key,
|
|
2687
|
+
role: 'option',
|
|
2688
|
+
tabIndex: 0,
|
|
2689
|
+
className: 'dsh-powerup-update-option',
|
|
2690
|
+
'aria-selected': isChosen ? 'true' : 'false',
|
|
2691
|
+
onClick: () => chooseRegistry(choice.key),
|
|
2692
|
+
onKeyDown: (event) => {
|
|
2693
|
+
if (event.key !== 'Enter' && event.key !== ' ') return;
|
|
2694
|
+
event.preventDefault();
|
|
2695
|
+
chooseRegistry(choice.key);
|
|
2696
|
+
},
|
|
2697
|
+
},
|
|
2698
|
+
h('span', { className: 'dsh-powerup-update-option-copy' },
|
|
2699
|
+
h('span', { className: 'dsh-powerup-update-option-title' }, choice.label),
|
|
2700
|
+
h('span', { className: 'dsh-powerup-update-option-host' }, choice.hint),
|
|
2701
|
+
),
|
|
2702
|
+
isDefault
|
|
2703
|
+
? h('span', { className: 'dsh-powerup-update-default-tag', title: t('update.defaultHint') }, t('update.defaultTag'))
|
|
2704
|
+
: h('button', {
|
|
2705
|
+
type: 'button',
|
|
2706
|
+
className: 'dsh-powerup-update-default',
|
|
2707
|
+
title: t('update.setDefault'),
|
|
2708
|
+
'aria-label': `${t('update.setDefault')} ${choice.label}`,
|
|
2709
|
+
onClick: (event) => {
|
|
2710
|
+
event.preventDefault();
|
|
2711
|
+
event.stopPropagation();
|
|
2712
|
+
setDefaultRegistry(choice);
|
|
2713
|
+
},
|
|
2714
|
+
}, h(StarIcon, { size: 12 })),
|
|
2715
|
+
isChosen ? h('span', { className: 'dsh-powerup-update-check' }, h(CheckIcon, { size: 13 })) : null,
|
|
2716
|
+
);
|
|
2717
|
+
}))
|
|
2306
2718
|
: null,
|
|
2307
2719
|
);
|
|
2308
2720
|
}
|
|
@@ -2677,6 +3089,7 @@
|
|
|
2677
3089
|
PowerUpOverlay,
|
|
2678
3090
|
PluginUpdateOverlay,
|
|
2679
3091
|
ensureUpdateButtons,
|
|
3092
|
+
ensureRestartBar,
|
|
2680
3093
|
updateButtonLabel,
|
|
2681
3094
|
popoverPlacement,
|
|
2682
3095
|
rowKeyParts,
|
|
@@ -2690,6 +3103,10 @@
|
|
|
2690
3103
|
compareVersionsDesc,
|
|
2691
3104
|
buildRegistryChoices,
|
|
2692
3105
|
pickRegistryChoice,
|
|
3106
|
+
matchStoredChoice,
|
|
3107
|
+
readDefaultRegistry,
|
|
3108
|
+
writeDefaultRegistry,
|
|
3109
|
+
readPendingRestarts,
|
|
2693
3110
|
lookupRegistries,
|
|
2694
3111
|
fetchRegistryVersions,
|
|
2695
3112
|
registryHost,
|
package/index.js
CHANGED
|
@@ -1,23 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Host half of dsh-powerup.
|
|
3
3
|
*
|
|
4
|
-
* The plugin is primarily a Client UI upgrade. This Host entry
|
|
5
|
-
*
|
|
6
|
-
* desktop application after a plugin update. It exposes a small loopback-only
|
|
7
|
-
* route under `/dsh-powerup/api`:
|
|
4
|
+
* The plugin is primarily a Client UI upgrade. This Host entry owns the two
|
|
5
|
+
* operations the browser cannot perform itself:
|
|
8
6
|
*
|
|
9
|
-
* GET /dsh-powerup/api/restart
|
|
10
|
-
* POST /dsh-powerup/api/restart
|
|
7
|
+
* GET /dsh-powerup/api/restart → { ok, supported, reason? }
|
|
8
|
+
* POST /dsh-powerup/api/restart → schedules a whole-app restart
|
|
9
|
+
* POST /dsh-powerup/api/self-update → removes and reinstalls this plugin
|
|
10
|
+
* in the background, surviving the
|
|
11
|
+
* plugin's own unload
|
|
11
12
|
*
|
|
12
|
-
* The Client half drives
|
|
13
|
-
* `pluginManager` Remote
|
|
13
|
+
* The Client half drives other plugins' installation through the built-in
|
|
14
|
+
* `pluginManager` Remote. Only the process restart and the self-update handoff
|
|
15
|
+
* need Host code.
|
|
14
16
|
*/
|
|
15
17
|
|
|
18
|
+
import { randomUUID } from 'node:crypto';
|
|
19
|
+
import { appendFileSync, mkdirSync } from 'node:fs';
|
|
20
|
+
import { dirname, join } from 'node:path';
|
|
21
|
+
import { homedir } from 'node:os';
|
|
16
22
|
import { describeRestart, requestRestart } from './lib/restart.js';
|
|
17
23
|
|
|
18
24
|
/** Prefix owned by this plugin. */
|
|
19
25
|
const ROUTE = '/dsh-powerup/api';
|
|
20
26
|
|
|
27
|
+
/** The bundle this pair of routes may replace while its own UI unloads. */
|
|
28
|
+
const MODULE_ID = '@restregular/dsh-powerup';
|
|
29
|
+
|
|
30
|
+
/** Let the HTTP response reach the browser before the plugin unloads. */
|
|
31
|
+
const SELF_UPDATE_DELAY_MS = 600;
|
|
32
|
+
|
|
33
|
+
/** Request body bound for the small JSON commands. */
|
|
34
|
+
const BODY_LIMIT = 16 * 1024;
|
|
35
|
+
|
|
21
36
|
/**
|
|
22
37
|
* @param {import('node:http').ServerResponse} res
|
|
23
38
|
* @param {unknown} payload
|
|
@@ -34,6 +49,23 @@ function sendJson(res, payload, status = 200) {
|
|
|
34
49
|
res.end(body);
|
|
35
50
|
}
|
|
36
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Read a bounded JSON request body.
|
|
54
|
+
* @param {import('node:http').IncomingMessage} req
|
|
55
|
+
* @returns {Promise<unknown>}
|
|
56
|
+
*/
|
|
57
|
+
async function readJsonBody(req) {
|
|
58
|
+
const chunks = [];
|
|
59
|
+
let size = 0;
|
|
60
|
+
for await (const chunk of req) {
|
|
61
|
+
size += chunk.length;
|
|
62
|
+
if (size > BODY_LIMIT) throw new Error('body-too-large');
|
|
63
|
+
chunks.push(chunk);
|
|
64
|
+
}
|
|
65
|
+
if (size === 0) return {};
|
|
66
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
67
|
+
}
|
|
68
|
+
|
|
37
69
|
/**
|
|
38
70
|
* The request came from this machine.
|
|
39
71
|
* @param {import('node:http').IncomingMessage} req
|
|
@@ -64,11 +96,89 @@ function sameOrigin(req) {
|
|
|
64
96
|
}
|
|
65
97
|
}
|
|
66
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Append one line to the self-update log; logging never blocks the operation.
|
|
101
|
+
* @param {string} line
|
|
102
|
+
*/
|
|
103
|
+
function appendSelfUpdateLog(line) {
|
|
104
|
+
try {
|
|
105
|
+
const path = join(process.env.DSH_HOME?.trim() || join(homedir(), '.dsh'), 'powerup', 'self-update.log');
|
|
106
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
107
|
+
appendFileSync(path, `${new Date().toISOString()} ${line}\n`);
|
|
108
|
+
} catch {
|
|
109
|
+
/* The diagnostic log is optional. */
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The concrete versions the update dialog accepts. The Client applies the same
|
|
115
|
+
* shape before it removes anything; this is the Host-side guard.
|
|
116
|
+
* @param {unknown} value
|
|
117
|
+
* @returns {string}
|
|
118
|
+
*/
|
|
119
|
+
function normalizeVersion(value) {
|
|
120
|
+
const text = String(value ?? '').trim().replace(/^v/u, '');
|
|
121
|
+
if (text === '' || /\s/u.test(text) || text.includes('@')) return '';
|
|
122
|
+
return /^\d+(?:\.\d+)*(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u.test(text) ? text : '';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Remove and reinstall this plugin through the built-in manager. The function
|
|
127
|
+
* is started after the HTTP response and keeps running in the Host process
|
|
128
|
+
* even when removing the bundle unloads this plugin's own Host fiber.
|
|
129
|
+
*
|
|
130
|
+
* @param {import('@deepseek-ai/cordis').Context} ctx
|
|
131
|
+
* @param {string} version
|
|
132
|
+
* @param {string | null} registry
|
|
133
|
+
* @param {{ manager?: unknown, log?: (line: string) => void }} [options]
|
|
134
|
+
* @returns {Promise<{ ok: boolean, stage?: string, error?: string }>}
|
|
135
|
+
*/
|
|
136
|
+
export async function performSelfUpdate(ctx, version, registry, options = {}) {
|
|
137
|
+
const log = options.log ?? appendSelfUpdateLog;
|
|
138
|
+
const manager = options.manager ?? ctx?.get?.('pluginManager');
|
|
139
|
+
if (manager === undefined || typeof manager.installBundle !== 'function') {
|
|
140
|
+
log('self-update: pluginManager service is unavailable');
|
|
141
|
+
return { ok: false, stage: 'manager', error: 'pluginManager service is unavailable' };
|
|
142
|
+
}
|
|
143
|
+
const spec = `${MODULE_ID}@${version}`;
|
|
144
|
+
log(`self-update: start ${spec} registry=${String(registry)}`);
|
|
145
|
+
let stage = 'remove';
|
|
146
|
+
try {
|
|
147
|
+
const removed = await manager.removeBundle(MODULE_ID);
|
|
148
|
+
if (removed?.error !== undefined) {
|
|
149
|
+
throw new Error(`remove failed: ${removed.error.code ?? 'error'} ${removed.error.diagnostic ?? ''}`.trim());
|
|
150
|
+
}
|
|
151
|
+
log(`self-update: removed (application=${removed?.application ?? 'unknown'})`);
|
|
152
|
+
stage = 'inspect';
|
|
153
|
+
const checked = await manager.inspect(spec, { registry });
|
|
154
|
+
if (checked?.status !== 'accepted') {
|
|
155
|
+
throw new Error(`inspect refused: ${checked?.problem ?? 'unknown'} ${checked?.reason ?? ''}`.trim());
|
|
156
|
+
}
|
|
157
|
+
log(`self-update: inspected (bundle=${String(checked.bundle)})`);
|
|
158
|
+
stage = 'install';
|
|
159
|
+
const installed = await manager.installBundle(spec, {
|
|
160
|
+
enabled: true,
|
|
161
|
+
registry,
|
|
162
|
+
requestId: randomUUID(),
|
|
163
|
+
});
|
|
164
|
+
if (installed?.error !== undefined) {
|
|
165
|
+
throw new Error(`install failed: ${installed.error.code ?? 'error'} ${installed.error.diagnostic ?? ''}`.trim());
|
|
166
|
+
}
|
|
167
|
+
log(`self-update: installed (application=${installed?.application ?? 'unknown'}, version=${installed?.version ?? 'unknown'})`);
|
|
168
|
+
return { ok: true, stage: 'install' };
|
|
169
|
+
} catch (error) {
|
|
170
|
+
const message = String(error?.message ?? error);
|
|
171
|
+
log(`self-update: failed at ${stage}: ${message}${error?.stack === undefined ? '' : `\n${error.stack}`}`);
|
|
172
|
+
return { ok: false, stage, error: message };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
67
176
|
/**
|
|
68
177
|
* @param {import('node:http').IncomingMessage} req
|
|
69
178
|
* @param {import('node:http').ServerResponse} res
|
|
179
|
+
* @param {import('@deepseek-ai/cordis').Context} ctx
|
|
70
180
|
*/
|
|
71
|
-
async function handle(req, res) {
|
|
181
|
+
async function handle(req, res, ctx) {
|
|
72
182
|
if (!isLoopback(req) || !sameOrigin(req)) {
|
|
73
183
|
sendJson(res, { ok: false, error: 'forbidden' }, 403);
|
|
74
184
|
return;
|
|
@@ -76,11 +186,44 @@ async function handle(req, res) {
|
|
|
76
186
|
|
|
77
187
|
const url = new URL(req.url ?? ROUTE, 'http://localhost');
|
|
78
188
|
const op = url.pathname.slice(ROUTE.length).replace(/^\/+/u, '') || url.searchParams.get('op') || '';
|
|
79
|
-
if (op !== '' && op !== 'restart') {
|
|
189
|
+
if (op !== '' && op !== 'restart' && op !== 'self-update') {
|
|
80
190
|
sendJson(res, { ok: false, error: 'not-found' }, 404);
|
|
81
191
|
return;
|
|
82
192
|
}
|
|
83
193
|
|
|
194
|
+
if (op === 'self-update') {
|
|
195
|
+
if (req.method !== 'POST') {
|
|
196
|
+
sendJson(res, { ok: false, error: 'method-not-allowed' }, 405);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
let body;
|
|
200
|
+
try {
|
|
201
|
+
body = await readJsonBody(req);
|
|
202
|
+
} catch (error) {
|
|
203
|
+
sendJson(res, { ok: false, error: String(error?.message ?? error) }, 400);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const version = normalizeVersion(body?.version);
|
|
207
|
+
if (version === '') {
|
|
208
|
+
sendJson(res, { ok: false, code: 'invalid-version', error: 'a concrete version is required' }, 400);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const registry = typeof body?.registry === 'string' && body.registry.trim() !== '' ? body.registry.trim() : null;
|
|
212
|
+
const manager = ctx?.get?.('pluginManager');
|
|
213
|
+
if (manager === undefined || typeof manager.removeBundle !== 'function' || typeof manager.installBundle !== 'function' || typeof manager.inspect !== 'function') {
|
|
214
|
+
sendJson(res, { ok: false, code: 'manager-unavailable', error: 'pluginManager service is unavailable' }, 503);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
sendJson(res, { ok: true, started: true, version });
|
|
218
|
+
const timer = setTimeout(() => {
|
|
219
|
+
void performSelfUpdate(ctx, version, registry, { manager }).catch((error) => {
|
|
220
|
+
appendSelfUpdateLog(`self-update: uncaught ${String(error?.stack ?? error)}`);
|
|
221
|
+
});
|
|
222
|
+
}, SELF_UPDATE_DELAY_MS);
|
|
223
|
+
timer.unref?.();
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
84
227
|
if (req.method === 'GET') {
|
|
85
228
|
sendJson(res, { ok: true, action: 'restart', ...describeRestart() });
|
|
86
229
|
return;
|
|
@@ -100,7 +243,7 @@ async function handle(req, res) {
|
|
|
100
243
|
}
|
|
101
244
|
|
|
102
245
|
/**
|
|
103
|
-
* Mount the restart
|
|
246
|
+
* Mount the restart and self-update routes once the Web server exists.
|
|
104
247
|
* @param {import('@deepseek-ai/cordis').Context} ctx
|
|
105
248
|
*/
|
|
106
249
|
export function apply(ctx) {
|
|
@@ -109,11 +252,15 @@ export function apply(ctx) {
|
|
|
109
252
|
const webServer = scope.get('webServer');
|
|
110
253
|
if (webServer === undefined || typeof webServer.register !== 'function') return;
|
|
111
254
|
scope.effect(
|
|
112
|
-
() => webServer.register({
|
|
113
|
-
|
|
255
|
+
() => webServer.register({
|
|
256
|
+
kind: 'prefix',
|
|
257
|
+
path: ROUTE,
|
|
258
|
+
handler: (req, res) => handle(req, res, ctx),
|
|
259
|
+
}),
|
|
260
|
+
'dsh-powerup: restart and self-update routes',
|
|
114
261
|
);
|
|
115
262
|
});
|
|
116
263
|
}
|
|
117
264
|
|
|
118
265
|
/** Exported for tests only; the Host never reads this. */
|
|
119
|
-
export const __test = { handle, isLoopback, sameOrigin, ROUTE };
|
|
266
|
+
export const __test = { handle, isLoopback, sameOrigin, performSelfUpdate, normalizeVersion, ROUTE, MODULE_ID };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@restregular/dsh-powerup",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Power-Up — small, opt-in upgrades for DeepSeek Harness. Adds a right-click action menu to the Trajectory ledger and an update control for third-party plugins installed from the Plugins page, with version lookup, uninstall-and-reinstall through the Add plugin path, and an optional DSH restart. · 为 DeepSeek Harness 提供小型增强:在轨迹页面右键记录即可复制、引用、查看详情或从该记录 Fork
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Power-Up — small, opt-in upgrades for DeepSeek Harness. Adds a right-click action menu to the Trajectory ledger and an update control for third-party plugins installed from the Plugins page, with version lookup from the configured registries, a starred default source, uninstall-and-reinstall through the Add plugin path, restart-needed markers, a background self-update, and an optional DSH restart. · 为 DeepSeek Harness 提供小型增强:在轨迹页面右键记录即可复制、引用、查看详情或从该记录 Fork;并在插件页为已安装插件提供带版本查询的更新按钮,安装源可设默认,沿用「添加插件」的安装流程,支持待重启标记、后台自更新与一键重启。",
|
|
5
5
|
"icon": "./icon.svg",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "index.js",
|