@rezti/dsh-rez-suite 0.1.19 → 0.1.20
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 +1 -1
- package/lib/client.d.ts +14 -0
- package/lib/client.js +142 -5
- package/lib/index.js +178 -3
- package/lib/style.css +81 -0
- package/package.json +8 -7
- package/src/client/api.ts +12 -0
- package/src/client/index.ts +14 -0
- package/src/client/locales.ts +14 -0
- package/src/client/panel/panel.module.css +82 -0
- package/src/client/upgrade-button.tsx +115 -0
- package/src/index.ts +2 -0
- package/src/protocol.ts +13 -0
- package/src/routes.ts +30 -2
- package/src/self-update.ts +155 -0
package/README.md
CHANGED
package/lib/client.d.ts
CHANGED
|
@@ -7349,6 +7349,13 @@ declare const zh: {
|
|
|
7349
7349
|
readonly 'weixin.listening': "通道在听:微信 → 网页「微信」会话(同一条,不另开窗口)→ 回微信。";
|
|
7350
7350
|
readonly 'weixin.loggedIn': "已登录,通道未在听。点「重新连接」即可拉起,不必再扫码。";
|
|
7351
7351
|
readonly 'weixin.loggedOut': "尚未登录。";
|
|
7352
|
+
readonly 'upgrade.label': "升级";
|
|
7353
|
+
readonly 'upgrade.current': "已最新";
|
|
7354
|
+
readonly 'upgrade.aria': "升级 ReZ-TI 插件";
|
|
7355
|
+
readonly 'upgrade.running': "升级中…";
|
|
7356
|
+
readonly 'upgrade.reloading': "正在重载";
|
|
7357
|
+
readonly 'upgrade.already': "已是 {version}";
|
|
7358
|
+
readonly 'upgrade.reloadTimeout': "升级完成,但网页还没重新连上。请刷新这一页。";
|
|
7352
7359
|
};
|
|
7353
7360
|
type RezKey = keyof typeof zh;
|
|
7354
7361
|
//#endregion
|
|
@@ -7372,6 +7379,13 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
|
7372
7379
|
children?: never;
|
|
7373
7380
|
};
|
|
7374
7381
|
};
|
|
7382
|
+
'sidebar.footer.action': {
|
|
7383
|
+
kind: 'list';
|
|
7384
|
+
scope: 'root';
|
|
7385
|
+
owner: {
|
|
7386
|
+
wide: boolean;
|
|
7387
|
+
};
|
|
7388
|
+
};
|
|
7375
7389
|
}
|
|
7376
7390
|
}
|
|
7377
7391
|
declare const inject: string[];
|
package/lib/client.js
CHANGED
|
@@ -4,7 +4,7 @@ window.__ModuleLoader__.load({
|
|
|
4
4
|
var module = { exports: {} };
|
|
5
5
|
var exports = module.exports;
|
|
6
6
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
-
(function(){var id="dsh-rez-suite-css";if(typeof document==="undefined"||document.getElementById(id))return;var s=document.createElement("style");s.id=id;s.textContent=".LOjPTW_shell {\n flex-direction: column;\n gap: 12px;\n min-width: 0;\n display: flex;\n}\n\n.LOjPTW_card {\n border: 1px solid var(--dsw-alias-border-l2);\n background: var(--dsw-alias-bg-layer-3);\n border-radius: 12px;\n list-style: none;\n transition: border-color .16s, background .16s;\n}\n\n.LOjPTW_card:hover {\n border-color: var(--dsw-alias-label-dimmed);\n}\n\n.LOjPTW_cardOpen {\n background: var(--dsw-alias-bg-layer-2);\n border-color: var(--dsw-alias-label-dimmed);\n}\n\n.LOjPTW_header {\n appearance: none;\n width: 100%;\n font: inherit;\n color: inherit;\n text-align: left;\n cursor: pointer;\n background: none;\n border: 0;\n border-radius: 12px;\n align-items: center;\n gap: 12px;\n padding: 14px 16px;\n display: flex;\n}\n\n.LOjPTW_header:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: -2px;\n}\n\n.LOjPTW_headText {\n flex-direction: column;\n flex: 1;\n gap: 4px;\n min-width: 0;\n display: flex;\n}\n\n.LOjPTW_name {\n color: var(--dsw-alias-label-primary);\n font-size: 15px;\n font-weight: 600;\n line-height: 1.4;\n}\n\n.LOjPTW_description {\n color: var(--dsw-alias-label-tertiary);\n font-size: 13px;\n line-height: 1.5;\n}\n\n.LOjPTW_chevron {\n color: var(--dsw-alias-label-tertiary);\n flex: none;\n transition: transform .16s;\n}\n\n.LOjPTW_chevronOpen {\n transform: rotate(180deg);\n}\n\n.LOjPTW_body {\n border-top: 1px solid var(--dsw-alias-border-l2);\n margin: 0 16px 12px;\n padding-top: 8px;\n}\n\n.LOjPTW_tabBar {\n border-bottom: 1px solid var(--dsw-alias-border-l1);\n flex: none;\n gap: 2px;\n display: flex;\n}\n\n.LOjPTW_tab {\n color: var(--dsw-alias-label-secondary);\n cursor: pointer;\n background: none;\n border: none;\n border-bottom: 2px solid #0000;\n border-radius: 6px 6px 0 0;\n padding: 7px 14px;\n font-size: 13px;\n}\n\n.LOjPTW_tab:hover {\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.LOjPTW_tab[data-active] {\n color: var(--dsw-alias-label-primary);\n border-bottom-color: var(--dsw-alias-state-business-primary);\n font-weight: 600;\n}\n\n.LOjPTW_tabBody {\n flex-direction: column;\n gap: 12px;\n min-width: 0;\n padding: 12px 0 4px;\n display: flex;\n}\n\n.LOjPTW_section {\n border: 1px solid var(--dsw-alias-border-l1);\n background: var(--dsw-alias-bg-layer-1, var(--dsw-alias-bg-base));\n border-radius: 10px;\n flex-direction: column;\n gap: 8px;\n padding: 12px;\n display: flex;\n}\n\n.LOjPTW_sectionTitle {\n color: var(--dsw-alias-label-primary);\n margin: 0;\n font-size: 13px;\n font-weight: 600;\n}\n\n.LOjPTW_field {\n flex-direction: column;\n align-items: stretch;\n gap: 4px;\n display: flex;\n}\n\n.LOjPTW_label {\n color: var(--dsw-alias-label-secondary);\n font-size: 12.5px;\n}\n\n.LOjPTW_input, .LOjPTW_select {\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-specific-input-major, var(--dsw-alias-bg-layer-3));\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 8px;\n outline: none;\n padding: 6px 8px;\n font-size: 13px;\n}\n\n.LOjPTW_checkbox {\n justify-self: start;\n width: 15px;\n height: 15px;\n}\n\n.LOjPTW_toolbar {\n flex-wrap: wrap;\n flex: none;\n align-items: center;\n gap: 8px;\n display: flex;\n}\n\n.LOjPTW_button {\n border: 1px solid var(--dsw-alias-border-l2);\n background: var(--dsw-specific-input-major, var(--dsw-alias-bg-layer-3));\n color: var(--dsw-alias-label-primary);\n cursor: pointer;\n border-radius: 8px;\n padding: 7px 12px;\n font-size: 13px;\n}\n\n.LOjPTW_button:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.LOjPTW_button:disabled {\n opacity: .4;\n cursor: default;\n}\n\n.LOjPTW_primaryButton {\n background: var(--dsw-alias-label-primary);\n border-color: var(--dsw-alias-label-primary);\n color: var(--dsw-alias-bg-layer-3);\n}\n\n.LOjPTW_dangerButton {\n background: var(--dsw-alias-state-danger, #c62828);\n border-color: var(--dsw-alias-state-danger, #c62828);\n color: #fff;\n}\n\n.LOjPTW_qr {\n background: #fff;\n border-radius: 8px;\n width: 200px;\n height: 200px;\n margin: 8px 0;\n display: block;\n}\n\n.LOjPTW_message {\n color: var(--dsw-alias-label-secondary);\n margin: 0;\n font-size: 12.5px;\n line-height: 1.5;\n}\n\n.LOjPTW_error {\n color: var(--dsw-alias-state-danger, #c62828);\n}\n\n.LOjPTW_tableWrap {\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n overflow: auto;\n}\n\n.LOjPTW_table {\n border-collapse: collapse;\n width: 100%;\n font-size: 12.5px;\n}\n\n.LOjPTW_table th {\n z-index: 1;\n text-align: left;\n background: var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-layer-1));\n padding: 8px 10px;\n position: sticky;\n top: 0;\n}\n\n.LOjPTW_table td {\n border-top: 1px solid var(--dsw-alias-border-l1);\n vertical-align: top;\n padding: 8px 10px;\n}\n\n.LOjPTW_badge {\n border-radius: 999px;\n padding: 2px 8px;\n font-size: 11px;\n display: inline-block;\n}\n\n.LOjPTW_badgeOk {\n color: #229a55;\n background: #229a5524;\n}\n\n.LOjPTW_badgeFail {\n color: #c62828;\n background: #c6282824;\n}\n\n.LOjPTW_cards {\n grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));\n gap: 8px;\n display: grid;\n}\n\n.LOjPTW_stat {\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n padding: 10px;\n}\n\n.LOjPTW_cardLabel {\n color: var(--dsw-alias-label-tertiary);\n font-size: 11px;\n}\n\n.LOjPTW_cardValue {\n color: var(--dsw-alias-label-primary);\n font-size: 15px;\n font-weight: 600;\n}\n";document.head.appendChild(s);})();
|
|
7
|
+
(function(){var id="dsh-rez-suite-css";if(typeof document==="undefined"||document.getElementById(id))return;var s=document.createElement("style");s.id=id;s.textContent=".LOjPTW_shell {\n flex-direction: column;\n gap: 12px;\n min-width: 0;\n display: flex;\n}\n\n.LOjPTW_card {\n border: 1px solid var(--dsw-alias-border-l2);\n background: var(--dsw-alias-bg-layer-3);\n border-radius: 12px;\n list-style: none;\n transition: border-color .16s, background .16s;\n}\n\n.LOjPTW_card:hover {\n border-color: var(--dsw-alias-label-dimmed);\n}\n\n.LOjPTW_cardOpen {\n background: var(--dsw-alias-bg-layer-2);\n border-color: var(--dsw-alias-label-dimmed);\n}\n\n.LOjPTW_header {\n appearance: none;\n width: 100%;\n font: inherit;\n color: inherit;\n text-align: left;\n cursor: pointer;\n background: none;\n border: 0;\n border-radius: 12px;\n align-items: center;\n gap: 12px;\n padding: 14px 16px;\n display: flex;\n}\n\n.LOjPTW_header:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: -2px;\n}\n\n.LOjPTW_headText {\n flex-direction: column;\n flex: 1;\n gap: 4px;\n min-width: 0;\n display: flex;\n}\n\n.LOjPTW_name {\n color: var(--dsw-alias-label-primary);\n font-size: 15px;\n font-weight: 600;\n line-height: 1.4;\n}\n\n.LOjPTW_description {\n color: var(--dsw-alias-label-tertiary);\n font-size: 13px;\n line-height: 1.5;\n}\n\n.LOjPTW_chevron {\n color: var(--dsw-alias-label-tertiary);\n flex: none;\n transition: transform .16s;\n}\n\n.LOjPTW_chevronOpen {\n transform: rotate(180deg);\n}\n\n.LOjPTW_body {\n border-top: 1px solid var(--dsw-alias-border-l2);\n margin: 0 16px 12px;\n padding-top: 8px;\n}\n\n.LOjPTW_tabBar {\n border-bottom: 1px solid var(--dsw-alias-border-l1);\n flex: none;\n gap: 2px;\n display: flex;\n}\n\n.LOjPTW_tab {\n color: var(--dsw-alias-label-secondary);\n cursor: pointer;\n background: none;\n border: none;\n border-bottom: 2px solid #0000;\n border-radius: 6px 6px 0 0;\n padding: 7px 14px;\n font-size: 13px;\n}\n\n.LOjPTW_tab:hover {\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.LOjPTW_tab[data-active] {\n color: var(--dsw-alias-label-primary);\n border-bottom-color: var(--dsw-alias-state-business-primary);\n font-weight: 600;\n}\n\n.LOjPTW_tabBody {\n flex-direction: column;\n gap: 12px;\n min-width: 0;\n padding: 12px 0 4px;\n display: flex;\n}\n\n.LOjPTW_section {\n border: 1px solid var(--dsw-alias-border-l1);\n background: var(--dsw-alias-bg-layer-1, var(--dsw-alias-bg-base));\n border-radius: 10px;\n flex-direction: column;\n gap: 8px;\n padding: 12px;\n display: flex;\n}\n\n.LOjPTW_sectionTitle {\n color: var(--dsw-alias-label-primary);\n margin: 0;\n font-size: 13px;\n font-weight: 600;\n}\n\n.LOjPTW_field {\n flex-direction: column;\n align-items: stretch;\n gap: 4px;\n display: flex;\n}\n\n.LOjPTW_label {\n color: var(--dsw-alias-label-secondary);\n font-size: 12.5px;\n}\n\n.LOjPTW_input, .LOjPTW_select {\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-specific-input-major, var(--dsw-alias-bg-layer-3));\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 8px;\n outline: none;\n padding: 6px 8px;\n font-size: 13px;\n}\n\n.LOjPTW_checkbox {\n justify-self: start;\n width: 15px;\n height: 15px;\n}\n\n.LOjPTW_toolbar {\n flex-wrap: wrap;\n flex: none;\n align-items: center;\n gap: 8px;\n display: flex;\n}\n\n.LOjPTW_button {\n border: 1px solid var(--dsw-alias-border-l2);\n background: var(--dsw-specific-input-major, var(--dsw-alias-bg-layer-3));\n color: var(--dsw-alias-label-primary);\n cursor: pointer;\n border-radius: 8px;\n padding: 7px 12px;\n font-size: 13px;\n}\n\n.LOjPTW_button:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.LOjPTW_button:disabled {\n opacity: .4;\n cursor: default;\n}\n\n.LOjPTW_primaryButton {\n background: var(--dsw-alias-label-primary);\n border-color: var(--dsw-alias-label-primary);\n color: var(--dsw-alias-bg-layer-3);\n}\n\n.LOjPTW_dangerButton {\n background: var(--dsw-alias-state-danger, #c62828);\n border-color: var(--dsw-alias-state-danger, #c62828);\n color: #fff;\n}\n\n.LOjPTW_qr {\n background: #fff;\n border-radius: 8px;\n width: 200px;\n height: 200px;\n margin: 8px 0;\n display: block;\n}\n\n.LOjPTW_message {\n color: var(--dsw-alias-label-secondary);\n margin: 0;\n font-size: 12.5px;\n line-height: 1.5;\n}\n\n.LOjPTW_error {\n color: var(--dsw-alias-state-danger, #c62828);\n}\n\n.LOjPTW_tableWrap {\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n overflow: auto;\n}\n\n.LOjPTW_table {\n border-collapse: collapse;\n width: 100%;\n font-size: 12.5px;\n}\n\n.LOjPTW_table th {\n z-index: 1;\n text-align: left;\n background: var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-layer-1));\n padding: 8px 10px;\n position: sticky;\n top: 0;\n}\n\n.LOjPTW_table td {\n border-top: 1px solid var(--dsw-alias-border-l1);\n vertical-align: top;\n padding: 8px 10px;\n}\n\n.LOjPTW_badge {\n border-radius: 999px;\n padding: 2px 8px;\n font-size: 11px;\n display: inline-block;\n}\n\n.LOjPTW_badgeOk {\n color: #229a55;\n background: #229a5524;\n}\n\n.LOjPTW_badgeFail {\n color: #c62828;\n background: #c6282824;\n}\n\n.LOjPTW_cards {\n grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));\n gap: 8px;\n display: grid;\n}\n\n.LOjPTW_stat {\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n padding: 10px;\n}\n\n.LOjPTW_cardLabel {\n color: var(--dsw-alias-label-tertiary);\n font-size: 11px;\n}\n\n.LOjPTW_cardValue {\n color: var(--dsw-alias-label-primary);\n font-size: 15px;\n font-weight: 600;\n}\n\n.LOjPTW_upgradeLayer {\n flex: none;\n align-items: center;\n width: 100%;\n height: 42px;\n margin: 8px 0 0;\n display: flex;\n position: relative;\n}\n\n.LOjPTW_upgradeLayerRail {\n width: 36px;\n height: 36px;\n margin: 0;\n}\n\n.LOjPTW_upgradeButton {\n width: calc(100% + 4px);\n height: 42px;\n color: var(--dsw-alias-label-primary);\n cursor: pointer;\n background: none;\n border: none;\n border-radius: 12px;\n align-items: center;\n gap: 8px;\n margin: 0 -2px;\n padding: 0 10px 0 8px;\n font-family: inherit;\n font-size: 14px;\n display: inline-flex;\n overflow: hidden;\n}\n\n.LOjPTW_upgradeButton:hover, .LOjPTW_upgradeButton[data-outdated] {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.LOjPTW_upgradeButton:disabled {\n cursor: progress;\n opacity: .72;\n}\n\n.LOjPTW_upgradeButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: -2px;\n}\n\n.LOjPTW_upgradeLayerRail .LOjPTW_upgradeButton {\n border-radius: 50%;\n justify-content: center;\n gap: 0;\n width: 36px;\n height: 36px;\n padding: 0;\n}\n\n.LOjPTW_upgradeLabel {\n text-overflow: ellipsis;\n white-space: nowrap;\n min-width: 0;\n overflow: hidden;\n}\n\n.LOjPTW_upgradeMeta {\n color: var(--dsw-alias-label-tertiary);\n font-variant-numeric: tabular-nums;\n flex: none;\n margin-left: auto;\n font-size: 12px;\n line-height: 16px;\n}\n\n.LOjPTW_upgradeDot {\n background: var(--dsw-alias-brand-primary);\n border-radius: 50%;\n flex: none;\n width: 7px;\n height: 7px;\n}\n";document.head.appendChild(s);})();
|
|
8
8
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
9
9
|
let react = require("react");
|
|
10
10
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
@@ -109,7 +109,14 @@ const zh = {
|
|
|
109
109
|
"weixin.qrAlt": "微信登录二维码",
|
|
110
110
|
"weixin.listening": "通道在听:微信 → 网页「微信」会话(同一条,不另开窗口)→ 回微信。",
|
|
111
111
|
"weixin.loggedIn": "已登录,通道未在听。点「重新连接」即可拉起,不必再扫码。",
|
|
112
|
-
"weixin.loggedOut": "尚未登录。"
|
|
112
|
+
"weixin.loggedOut": "尚未登录。",
|
|
113
|
+
"upgrade.label": "升级",
|
|
114
|
+
"upgrade.current": "已最新",
|
|
115
|
+
"upgrade.aria": "升级 ReZ-TI 插件",
|
|
116
|
+
"upgrade.running": "升级中…",
|
|
117
|
+
"upgrade.reloading": "正在重载",
|
|
118
|
+
"upgrade.already": "已是 {version}",
|
|
119
|
+
"upgrade.reloadTimeout": "升级完成,但网页还没重新连上。请刷新这一页。"
|
|
113
120
|
};
|
|
114
121
|
const en = {
|
|
115
122
|
"entry.label": "Rez",
|
|
@@ -208,7 +215,14 @@ const en = {
|
|
|
208
215
|
"weixin.qrAlt": "WeChat login QR",
|
|
209
216
|
"weixin.listening": "Channel is live: WeChat → the WeChat folder session on the left → WeChat.",
|
|
210
217
|
"weixin.loggedIn": "Logged in, listener idle. Click Reconnect — no need to scan again.",
|
|
211
|
-
"weixin.loggedOut": "Not logged in."
|
|
218
|
+
"weixin.loggedOut": "Not logged in.",
|
|
219
|
+
"upgrade.label": "Upgrade",
|
|
220
|
+
"upgrade.current": "Up to date",
|
|
221
|
+
"upgrade.aria": "Upgrade the ReZ-TI plugin",
|
|
222
|
+
"upgrade.running": "Updating…",
|
|
223
|
+
"upgrade.reloading": "Reloading",
|
|
224
|
+
"upgrade.already": "Already {version}",
|
|
225
|
+
"upgrade.reloadTimeout": "Update finished, but the page did not reconnect. Refresh this tab."
|
|
212
226
|
};
|
|
213
227
|
/** Minimal {name} interpolator shared by the panel helpers. */
|
|
214
228
|
function t(dict, key, values) {
|
|
@@ -225,7 +239,8 @@ const REZ_API = {
|
|
|
225
239
|
audit: "/api/dsh-rez-suite/audit",
|
|
226
240
|
auditReset: "/api/dsh-rez-suite/audit/reset",
|
|
227
241
|
weixin: "/api/dsh-rez-suite/weixin",
|
|
228
|
-
weixinVerify: "/api/dsh-rez-suite/weixin/verify"
|
|
242
|
+
weixinVerify: "/api/dsh-rez-suite/weixin/verify",
|
|
243
|
+
update: "/api/dsh-rez-suite/update"
|
|
229
244
|
};
|
|
230
245
|
//#endregion
|
|
231
246
|
//#region src/client/api.ts
|
|
@@ -287,6 +302,12 @@ var RezApi = class {
|
|
|
287
302
|
async weixinLogout() {
|
|
288
303
|
return readJson(await fetch(REZ_API.weixin, { method: "DELETE" }));
|
|
289
304
|
}
|
|
305
|
+
async updateStatus() {
|
|
306
|
+
return readJson(await fetch(REZ_API.update));
|
|
307
|
+
}
|
|
308
|
+
async applyUpdate() {
|
|
309
|
+
return readJson(await fetch(REZ_API.update, { method: "POST" }));
|
|
310
|
+
}
|
|
290
311
|
};
|
|
291
312
|
//#endregion
|
|
292
313
|
//#region src/client/panel/helpers.ts
|
|
@@ -341,7 +362,13 @@ var panel_module_default = {
|
|
|
341
362
|
"tabBody": "LOjPTW_tabBody",
|
|
342
363
|
"table": "LOjPTW_table",
|
|
343
364
|
"tableWrap": "LOjPTW_tableWrap",
|
|
344
|
-
"toolbar": "LOjPTW_toolbar"
|
|
365
|
+
"toolbar": "LOjPTW_toolbar",
|
|
366
|
+
"upgradeButton": "LOjPTW_upgradeButton",
|
|
367
|
+
"upgradeDot": "LOjPTW_upgradeDot",
|
|
368
|
+
"upgradeLabel": "LOjPTW_upgradeLabel",
|
|
369
|
+
"upgradeLayer": "LOjPTW_upgradeLayer",
|
|
370
|
+
"upgradeLayerRail": "LOjPTW_upgradeLayerRail",
|
|
371
|
+
"upgradeMeta": "LOjPTW_upgradeMeta"
|
|
345
372
|
};
|
|
346
373
|
//#endregion
|
|
347
374
|
//#region src/client/panel/AuditTab.tsx
|
|
@@ -1249,6 +1276,109 @@ function RezPluginCard() {
|
|
|
1249
1276
|
});
|
|
1250
1277
|
}
|
|
1251
1278
|
//#endregion
|
|
1279
|
+
//#region src/client/upgrade-button.tsx
|
|
1280
|
+
/**
|
|
1281
|
+
* Sidebar footer action: upgrade @rezti/dsh-rez-suite without a terminal.
|
|
1282
|
+
* Host code still needs a process respawn; the button does that after pnpm add.
|
|
1283
|
+
*/
|
|
1284
|
+
function UpgradeIcon() {
|
|
1285
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
1286
|
+
width: "16",
|
|
1287
|
+
height: "16",
|
|
1288
|
+
viewBox: "0 0 16 16",
|
|
1289
|
+
"aria-hidden": "true",
|
|
1290
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
1291
|
+
d: "M8 12.5V4.8M8 4.8 4.8 8M8 4.8 11.2 8M3 13.2h10",
|
|
1292
|
+
fill: "none",
|
|
1293
|
+
stroke: "currentColor",
|
|
1294
|
+
strokeWidth: "1.4",
|
|
1295
|
+
strokeLinecap: "round",
|
|
1296
|
+
strokeLinejoin: "round"
|
|
1297
|
+
})
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
async function waitUntilHostBack(api, timeoutMs = 6e4) {
|
|
1301
|
+
const deadline = Date.now() + timeoutMs;
|
|
1302
|
+
await new Promise((resolve) => setTimeout(resolve, 800));
|
|
1303
|
+
while (Date.now() < deadline) try {
|
|
1304
|
+
await api.updateStatus();
|
|
1305
|
+
return;
|
|
1306
|
+
} catch {
|
|
1307
|
+
await new Promise((resolve) => setTimeout(resolve, 700));
|
|
1308
|
+
}
|
|
1309
|
+
throw new Error(tt("upgrade.reloadTimeout"));
|
|
1310
|
+
}
|
|
1311
|
+
function UpgradeButton({ wide }) {
|
|
1312
|
+
const api = (0, react.useMemo)(() => new RezApi(), []);
|
|
1313
|
+
const [info, setInfo] = (0, react.useState)();
|
|
1314
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
1315
|
+
const [note, setNote] = (0, react.useState)("");
|
|
1316
|
+
(0, react.useEffect)(() => {
|
|
1317
|
+
let cancelled = false;
|
|
1318
|
+
api.updateStatus().then((value) => {
|
|
1319
|
+
if (!cancelled) setInfo(value);
|
|
1320
|
+
}).catch((error) => {
|
|
1321
|
+
if (!cancelled) setNote(error instanceof Error ? error.message : String(error));
|
|
1322
|
+
});
|
|
1323
|
+
return () => {
|
|
1324
|
+
cancelled = true;
|
|
1325
|
+
};
|
|
1326
|
+
}, [api]);
|
|
1327
|
+
const label = busy ? tt("upgrade.running") : info?.outdated === true ? tt("upgrade.label") : tt("upgrade.current");
|
|
1328
|
+
const meta = note !== "" ? note : info === void 0 ? "" : info.outdated ? info.latest : info.installed;
|
|
1329
|
+
async function onClick() {
|
|
1330
|
+
if (busy) return;
|
|
1331
|
+
setBusy(true);
|
|
1332
|
+
setNote("");
|
|
1333
|
+
try {
|
|
1334
|
+
const snapshot = info ?? await api.updateStatus();
|
|
1335
|
+
setInfo(snapshot);
|
|
1336
|
+
if (!snapshot.outdated) {
|
|
1337
|
+
setNote(tt("upgrade.already", { version: snapshot.installed }));
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
const result = await api.applyUpdate();
|
|
1341
|
+
setInfo(result);
|
|
1342
|
+
if (result.restarting) {
|
|
1343
|
+
setNote(tt("upgrade.reloading"));
|
|
1344
|
+
await waitUntilHostBack(api);
|
|
1345
|
+
window.location.reload();
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
setNote(tt("upgrade.already", { version: result.installed }));
|
|
1349
|
+
} catch (error) {
|
|
1350
|
+
setNote(error instanceof Error ? error.message : String(error));
|
|
1351
|
+
} finally {
|
|
1352
|
+
setBusy(false);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1356
|
+
className: wide ? panel_module_default.upgradeLayer : `${panel_module_default.upgradeLayer} ${panel_module_default.upgradeLayerRail}`,
|
|
1357
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1358
|
+
type: "button",
|
|
1359
|
+
className: panel_module_default.upgradeButton,
|
|
1360
|
+
"data-outdated": info?.outdated === true ? "" : void 0,
|
|
1361
|
+
"aria-label": tt("upgrade.aria"),
|
|
1362
|
+
disabled: busy,
|
|
1363
|
+
onClick: () => {
|
|
1364
|
+
onClick();
|
|
1365
|
+
},
|
|
1366
|
+
children: [
|
|
1367
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UpgradeIcon, {}),
|
|
1368
|
+
wide && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1369
|
+
className: panel_module_default.upgradeLabel,
|
|
1370
|
+
children: label
|
|
1371
|
+
}),
|
|
1372
|
+
wide && meta !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1373
|
+
className: panel_module_default.upgradeMeta,
|
|
1374
|
+
children: meta
|
|
1375
|
+
}),
|
|
1376
|
+
!wide && info?.outdated === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: panel_module_default.upgradeDot })
|
|
1377
|
+
]
|
|
1378
|
+
})
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1381
|
+
//#endregion
|
|
1252
1382
|
//#region src/client/index.ts
|
|
1253
1383
|
const NS = "dsh-rez-suite";
|
|
1254
1384
|
const inject = ["slots", "locale"];
|
|
@@ -1270,6 +1400,13 @@ function apply(ctx) {
|
|
|
1270
1400
|
label: () => t("settings.title"),
|
|
1271
1401
|
locale: NS
|
|
1272
1402
|
}, RezSettingsPage));
|
|
1403
|
+
ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
|
|
1404
|
+
name: "sidebar.footer.action",
|
|
1405
|
+
id: "rez-upgrade",
|
|
1406
|
+
order: 10,
|
|
1407
|
+
label: () => t("upgrade.label"),
|
|
1408
|
+
locale: NS
|
|
1409
|
+
}, UpgradeButton));
|
|
1273
1410
|
}
|
|
1274
1411
|
//#endregion
|
|
1275
1412
|
exports.apply = apply;
|
package/lib/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { REZ_CREDENTIAL_REFS, REZ_DEFAULT_CONNECTIONS, REZ_SYSTEMS, lastMcpStart
|
|
|
5
5
|
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { DatabaseSync } from "node:sqlite";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
8
9
|
//#region ../../node_modules/.pnpm/@deepseek-ai+cosmokit@1.8.2/node_modules/@deepseek-ai/cosmokit/lib/index.js
|
|
9
10
|
/** Return true when a value is `null` or `undefined`. */
|
|
10
11
|
function isNullable(value) {
|
|
@@ -2955,7 +2956,8 @@ const REZ_API = {
|
|
|
2955
2956
|
audit: "/api/dsh-rez-suite/audit",
|
|
2956
2957
|
auditReset: "/api/dsh-rez-suite/audit/reset",
|
|
2957
2958
|
weixin: "/api/dsh-rez-suite/weixin",
|
|
2958
|
-
weixinVerify: "/api/dsh-rez-suite/weixin/verify"
|
|
2959
|
+
weixinVerify: "/api/dsh-rez-suite/weixin/verify",
|
|
2960
|
+
update: "/api/dsh-rez-suite/update"
|
|
2959
2961
|
};
|
|
2960
2962
|
//#endregion
|
|
2961
2963
|
//#region src/store.ts
|
|
@@ -3205,7 +3207,7 @@ function applyPublicPatch(current, patch) {
|
|
|
3205
3207
|
}
|
|
3206
3208
|
/** Build every /api/dsh-rez-suite route (exact paths). */
|
|
3207
3209
|
function makeRoutes(deps) {
|
|
3208
|
-
const { getConfig, updateConfig, host, tokens, onCredentialWritten } = deps;
|
|
3210
|
+
const { getConfig, updateConfig, host, tokens, onCredentialWritten, selfUpdate } = deps;
|
|
3209
3211
|
const guard = (req, res, method) => {
|
|
3210
3212
|
if (!isLoopbackRequest(req)) {
|
|
3211
3213
|
writeJson(res, 403, { error: "forbidden: loopback-only" });
|
|
@@ -3277,6 +3279,31 @@ function makeRoutes(deps) {
|
|
|
3277
3279
|
tokens.reset();
|
|
3278
3280
|
writeJson(res, 200, { ok: true });
|
|
3279
3281
|
}
|
|
3282
|
+
},
|
|
3283
|
+
{
|
|
3284
|
+
kind: "exact",
|
|
3285
|
+
path: REZ_API.update,
|
|
3286
|
+
handler: async (req, res) => {
|
|
3287
|
+
const method = req.method === "POST" ? "POST" : "GET";
|
|
3288
|
+
if (!guard(req, res, method)) return;
|
|
3289
|
+
if (selfUpdate === void 0) {
|
|
3290
|
+
writeJson(res, 501, { error: "self-update is not available" });
|
|
3291
|
+
return;
|
|
3292
|
+
}
|
|
3293
|
+
try {
|
|
3294
|
+
if (method === "GET") {
|
|
3295
|
+
writeJson(res, 200, await selfUpdate.status());
|
|
3296
|
+
return;
|
|
3297
|
+
}
|
|
3298
|
+
const result = await selfUpdate.apply();
|
|
3299
|
+
writeJson(res, 200, result);
|
|
3300
|
+
if (result.restarting) setTimeout(() => {
|
|
3301
|
+
selfUpdate.relaunch();
|
|
3302
|
+
}, 400);
|
|
3303
|
+
} catch (error) {
|
|
3304
|
+
writeJson(res, 502, { error: error instanceof Error ? error.message : String(error) });
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3280
3307
|
}
|
|
3281
3308
|
];
|
|
3282
3309
|
}
|
|
@@ -7832,6 +7859,153 @@ function mountBossDesk(ctx, getRole) {
|
|
|
7832
7859
|
return run;
|
|
7833
7860
|
}
|
|
7834
7861
|
//#endregion
|
|
7862
|
+
//#region src/self-update.ts
|
|
7863
|
+
/**
|
|
7864
|
+
* In-place upgrade of @rezti/dsh-rez-suite inside the running dsh profile.
|
|
7865
|
+
* pnpm add writes the new bits; Node cannot replace an already-imported host
|
|
7866
|
+
* module, so we respawn the same argv after the HTTP response flushes.
|
|
7867
|
+
*/
|
|
7868
|
+
const SUITE_PACKAGE = "@rezti/dsh-rez-suite";
|
|
7869
|
+
const NPM_LATEST = "https://registry.npmjs.org/@rezti/dsh-rez-suite/latest";
|
|
7870
|
+
const PNPM_TIMEOUT_MS = 18e4;
|
|
7871
|
+
function compareVersions(left, right) {
|
|
7872
|
+
const a = left.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
7873
|
+
const b = right.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
7874
|
+
const n = Math.max(a.length, b.length);
|
|
7875
|
+
for (let i = 0; i < n; i += 1) {
|
|
7876
|
+
const av = a[i] ?? 0;
|
|
7877
|
+
const bv = b[i] ?? 0;
|
|
7878
|
+
if (av > bv) return 1;
|
|
7879
|
+
if (av < bv) return -1;
|
|
7880
|
+
}
|
|
7881
|
+
return 0;
|
|
7882
|
+
}
|
|
7883
|
+
function readInstalledVersion(packageDir) {
|
|
7884
|
+
const raw = JSON.parse(readFileSync(join(packageDir, "package.json"), "utf8"));
|
|
7885
|
+
if (typeof raw.version !== "string" || raw.version === "") throw new Error("suite package.json missing version");
|
|
7886
|
+
return raw.version;
|
|
7887
|
+
}
|
|
7888
|
+
function findProfileDir(startDir) {
|
|
7889
|
+
let dir = startDir;
|
|
7890
|
+
for (let i = 0; i < 12; i += 1) {
|
|
7891
|
+
const manifest = join(dir, "package.json");
|
|
7892
|
+
if (existsSync(manifest)) try {
|
|
7893
|
+
const raw = JSON.parse(readFileSync(manifest, "utf8"));
|
|
7894
|
+
if (Array.isArray(raw.dsh?.profile?.bundles)) return dir;
|
|
7895
|
+
} catch {}
|
|
7896
|
+
const parent = dirname(dir);
|
|
7897
|
+
if (parent === dir) break;
|
|
7898
|
+
dir = parent;
|
|
7899
|
+
}
|
|
7900
|
+
throw new Error("cannot find dsh profile directory (no package.json with dsh.profile.bundles)");
|
|
7901
|
+
}
|
|
7902
|
+
function shellQuote(value) {
|
|
7903
|
+
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
7904
|
+
}
|
|
7905
|
+
function relaunchScript(argv, parentPid) {
|
|
7906
|
+
return `while kill -0 ${parentPid} 2>/dev/null; do sleep 0.15; done; sleep 0.25; exec ${argv.map(shellQuote).join(" ")}`;
|
|
7907
|
+
}
|
|
7908
|
+
async function fetchLatestVersion(fetchImpl = fetch) {
|
|
7909
|
+
const response = await fetchImpl(NPM_LATEST, { headers: { accept: "application/json" } });
|
|
7910
|
+
if (!response.ok) throw new Error(`npm registry HTTP ${response.status}`);
|
|
7911
|
+
const body = await response.json();
|
|
7912
|
+
if (typeof body.version !== "string" || body.version === "") throw new Error("npm registry missing version");
|
|
7913
|
+
return body.version;
|
|
7914
|
+
}
|
|
7915
|
+
function runPnpmAdd(cwd, spec, env = process.env) {
|
|
7916
|
+
return new Promise((resolve, reject) => {
|
|
7917
|
+
const child = spawn("pnpm", ["add", spec], {
|
|
7918
|
+
cwd,
|
|
7919
|
+
env,
|
|
7920
|
+
stdio: [
|
|
7921
|
+
"ignore",
|
|
7922
|
+
"pipe",
|
|
7923
|
+
"pipe"
|
|
7924
|
+
]
|
|
7925
|
+
});
|
|
7926
|
+
let stderr = "";
|
|
7927
|
+
child.stderr.on("data", (chunk) => {
|
|
7928
|
+
stderr += chunk.toString();
|
|
7929
|
+
});
|
|
7930
|
+
const timer = setTimeout(() => {
|
|
7931
|
+
child.kill("SIGTERM");
|
|
7932
|
+
reject(/* @__PURE__ */ new Error("pnpm add timed out after 3 minutes"));
|
|
7933
|
+
}, PNPM_TIMEOUT_MS);
|
|
7934
|
+
child.on("error", (error) => {
|
|
7935
|
+
clearTimeout(timer);
|
|
7936
|
+
reject(error);
|
|
7937
|
+
});
|
|
7938
|
+
child.on("close", (code) => {
|
|
7939
|
+
clearTimeout(timer);
|
|
7940
|
+
if (code === 0) resolve();
|
|
7941
|
+
else reject(new Error(stderr.trim() || `pnpm add exited ${String(code)}`));
|
|
7942
|
+
});
|
|
7943
|
+
});
|
|
7944
|
+
}
|
|
7945
|
+
function spawnRelaunch(argv, parentPid, cwd, env = process.env) {
|
|
7946
|
+
spawn("/bin/bash", ["-c", relaunchScript(argv, parentPid)], {
|
|
7947
|
+
cwd,
|
|
7948
|
+
env,
|
|
7949
|
+
detached: true,
|
|
7950
|
+
stdio: "ignore"
|
|
7951
|
+
}).unref();
|
|
7952
|
+
}
|
|
7953
|
+
let inflight;
|
|
7954
|
+
function createSelfUpdate(io) {
|
|
7955
|
+
const status = async () => {
|
|
7956
|
+
const installed = readInstalledVersion(io.packageDir);
|
|
7957
|
+
const latest = await io.fetchLatest();
|
|
7958
|
+
return {
|
|
7959
|
+
package: SUITE_PACKAGE,
|
|
7960
|
+
installed,
|
|
7961
|
+
latest,
|
|
7962
|
+
outdated: compareVersions(latest, installed) > 0
|
|
7963
|
+
};
|
|
7964
|
+
};
|
|
7965
|
+
const apply = async () => {
|
|
7966
|
+
if (inflight !== void 0) throw new Error("upgrade already running");
|
|
7967
|
+
const work = (async () => {
|
|
7968
|
+
const snapshot = await status();
|
|
7969
|
+
if (!snapshot.outdated) return {
|
|
7970
|
+
...snapshot,
|
|
7971
|
+
restarting: false
|
|
7972
|
+
};
|
|
7973
|
+
const profileDir = findProfileDir(io.packageDir);
|
|
7974
|
+
await io.install(profileDir, `${SUITE_PACKAGE}@${snapshot.latest}`);
|
|
7975
|
+
return {
|
|
7976
|
+
...snapshot,
|
|
7977
|
+
installed: snapshot.latest,
|
|
7978
|
+
outdated: false,
|
|
7979
|
+
restarting: true
|
|
7980
|
+
};
|
|
7981
|
+
})();
|
|
7982
|
+
inflight = work;
|
|
7983
|
+
try {
|
|
7984
|
+
return await work;
|
|
7985
|
+
} finally {
|
|
7986
|
+
inflight = void 0;
|
|
7987
|
+
}
|
|
7988
|
+
};
|
|
7989
|
+
return {
|
|
7990
|
+
status,
|
|
7991
|
+
apply,
|
|
7992
|
+
relaunch: io.relaunch
|
|
7993
|
+
};
|
|
7994
|
+
}
|
|
7995
|
+
function liveSelfUpdate() {
|
|
7996
|
+
return createSelfUpdate({
|
|
7997
|
+
packageDir: dirname(fileURLToPath(new URL(".", import.meta.url))),
|
|
7998
|
+
fetchLatest: () => fetchLatestVersion(),
|
|
7999
|
+
install: (profileDir, spec) => runPnpmAdd(profileDir, spec),
|
|
8000
|
+
relaunch: () => {
|
|
8001
|
+
spawnRelaunch(process.argv, process.pid, process.cwd());
|
|
8002
|
+
setTimeout(() => {
|
|
8003
|
+
process.exit(0);
|
|
8004
|
+
}, 50);
|
|
8005
|
+
}
|
|
8006
|
+
});
|
|
8007
|
+
}
|
|
8008
|
+
//#endregion
|
|
7835
8009
|
//#region src/index.ts
|
|
7836
8010
|
const name = "rez-suite";
|
|
7837
8011
|
const inject = [
|
|
@@ -7947,7 +8121,8 @@ function apply(ctx, config) {
|
|
|
7947
8121
|
onCredentialWritten: (ref) => {
|
|
7948
8122
|
const emit = ctx.emit;
|
|
7949
8123
|
emit("credentials/updated", ref);
|
|
7950
|
-
}
|
|
8124
|
+
},
|
|
8125
|
+
selfUpdate: liveSelfUpdate()
|
|
7951
8126
|
});
|
|
7952
8127
|
let disposeRoutes;
|
|
7953
8128
|
let disposeTools;
|
package/lib/style.css
CHANGED
|
@@ -287,3 +287,84 @@
|
|
|
287
287
|
font-size: 15px;
|
|
288
288
|
font-weight: 600;
|
|
289
289
|
}
|
|
290
|
+
|
|
291
|
+
.LOjPTW_upgradeLayer {
|
|
292
|
+
flex: none;
|
|
293
|
+
align-items: center;
|
|
294
|
+
width: 100%;
|
|
295
|
+
height: 42px;
|
|
296
|
+
margin: 8px 0 0;
|
|
297
|
+
display: flex;
|
|
298
|
+
position: relative;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.LOjPTW_upgradeLayerRail {
|
|
302
|
+
width: 36px;
|
|
303
|
+
height: 36px;
|
|
304
|
+
margin: 0;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.LOjPTW_upgradeButton {
|
|
308
|
+
width: calc(100% + 4px);
|
|
309
|
+
height: 42px;
|
|
310
|
+
color: var(--dsw-alias-label-primary);
|
|
311
|
+
cursor: pointer;
|
|
312
|
+
background: none;
|
|
313
|
+
border: none;
|
|
314
|
+
border-radius: 12px;
|
|
315
|
+
align-items: center;
|
|
316
|
+
gap: 8px;
|
|
317
|
+
margin: 0 -2px;
|
|
318
|
+
padding: 0 10px 0 8px;
|
|
319
|
+
font-family: inherit;
|
|
320
|
+
font-size: 14px;
|
|
321
|
+
display: inline-flex;
|
|
322
|
+
overflow: hidden;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.LOjPTW_upgradeButton:hover, .LOjPTW_upgradeButton[data-outdated] {
|
|
326
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.LOjPTW_upgradeButton:disabled {
|
|
330
|
+
cursor: progress;
|
|
331
|
+
opacity: .72;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.LOjPTW_upgradeButton:focus-visible {
|
|
335
|
+
outline: 2px solid var(--dsw-alias-brand-primary);
|
|
336
|
+
outline-offset: -2px;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.LOjPTW_upgradeLayerRail .LOjPTW_upgradeButton {
|
|
340
|
+
border-radius: 50%;
|
|
341
|
+
justify-content: center;
|
|
342
|
+
gap: 0;
|
|
343
|
+
width: 36px;
|
|
344
|
+
height: 36px;
|
|
345
|
+
padding: 0;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.LOjPTW_upgradeLabel {
|
|
349
|
+
text-overflow: ellipsis;
|
|
350
|
+
white-space: nowrap;
|
|
351
|
+
min-width: 0;
|
|
352
|
+
overflow: hidden;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.LOjPTW_upgradeMeta {
|
|
356
|
+
color: var(--dsw-alias-label-tertiary);
|
|
357
|
+
font-variant-numeric: tabular-nums;
|
|
358
|
+
flex: none;
|
|
359
|
+
margin-left: auto;
|
|
360
|
+
font-size: 12px;
|
|
361
|
+
line-height: 16px;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.LOjPTW_upgradeDot {
|
|
365
|
+
background: var(--dsw-alias-brand-primary);
|
|
366
|
+
border-radius: 50%;
|
|
367
|
+
flex: none;
|
|
368
|
+
width: 7px;
|
|
369
|
+
height: 7px;
|
|
370
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rezti/dsh-rez-suite",
|
|
3
3
|
"description": "ReZ-TI one-package install for DeepSeek Harness. Update with: dsh plugin --profile web update @rezti/dsh-rez-suite",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.20",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": "^22.19.0 || >=24.0.0"
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
"@deepseek-ai/dsh-client-connection",
|
|
31
31
|
"@deepseek-ai/dsh-client-locale",
|
|
32
32
|
"@deepseek-ai/dsh-client-ui-settings",
|
|
33
|
-
"@deepseek-ai/dsh-client-ui-settings-plugins"
|
|
33
|
+
"@deepseek-ai/dsh-client-ui-settings-plugins",
|
|
34
|
+
"@deepseek-ai/dsh-client-ui-sidebar"
|
|
34
35
|
],
|
|
35
36
|
"platform": "web"
|
|
36
37
|
}
|
|
@@ -38,13 +39,13 @@
|
|
|
38
39
|
"dependencies": {
|
|
39
40
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
40
41
|
"zod": "^4.4.3",
|
|
41
|
-
"@rezti/dsh-rez-
|
|
42
|
-
"@rezti/dsh-rez-odoo": "0.1.7",
|
|
43
|
-
"@rezti/dsh-rez-intent": "0.1.4",
|
|
44
|
-
"@rezti/dsh-rez-nextcloud": "0.1.7",
|
|
42
|
+
"@rezti/dsh-rez-token-manager": "0.1.4",
|
|
45
43
|
"@rezti/dsh-rez-sso": "0.1.7",
|
|
44
|
+
"@rezti/dsh-rez-intent": "0.1.4",
|
|
46
45
|
"@rezti/dsh-rez-wechat": "0.1.11",
|
|
47
|
-
"@rezti/dsh-rez-
|
|
46
|
+
"@rezti/dsh-rez-ha-bridge": "0.1.7",
|
|
47
|
+
"@rezti/dsh-rez-nextcloud": "0.1.7",
|
|
48
|
+
"@rezti/dsh-rez-odoo": "0.1.7"
|
|
48
49
|
},
|
|
49
50
|
"peerDependencies": {
|
|
50
51
|
"react": "^18.2.0",
|
package/src/client/api.ts
CHANGED
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
type RezServerStatus,
|
|
10
10
|
type RezStatusResponse,
|
|
11
11
|
type RezTestResult,
|
|
12
|
+
type RezUpdateApplyResult,
|
|
13
|
+
type RezUpdateStatus,
|
|
12
14
|
type RezWeixinStatus,
|
|
13
15
|
} from '../protocol.ts'
|
|
14
16
|
|
|
@@ -97,4 +99,14 @@ export class RezApi {
|
|
|
97
99
|
const response = await fetch(REZ_API.weixin, { method: 'DELETE' })
|
|
98
100
|
return readJson<RezWeixinStatus>(response)
|
|
99
101
|
}
|
|
102
|
+
|
|
103
|
+
async updateStatus(): Promise<RezUpdateStatus> {
|
|
104
|
+
const response = await fetch(REZ_API.update)
|
|
105
|
+
return readJson<RezUpdateStatus>(response)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async applyUpdate(): Promise<RezUpdateApplyResult> {
|
|
109
|
+
const response = await fetch(REZ_API.update, { method: 'POST' })
|
|
110
|
+
return readJson<RezUpdateApplyResult>(response)
|
|
111
|
+
}
|
|
100
112
|
}
|
package/src/client/index.ts
CHANGED
|
@@ -11,6 +11,7 @@ import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
|
11
11
|
import type {} from '@deepseek-ai/dsh-client-ui-slots'
|
|
12
12
|
import { en, zh, type RezKey } from './locales.ts'
|
|
13
13
|
import { RezPluginCard, RezSettingsPage } from './settings-card.tsx'
|
|
14
|
+
import { UpgradeButton } from './upgrade-button.tsx'
|
|
14
15
|
|
|
15
16
|
const NS = 'dsh-rez-suite'
|
|
16
17
|
|
|
@@ -29,6 +30,11 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
|
29
30
|
scope: 'root'
|
|
30
31
|
owner: { children?: never }
|
|
31
32
|
}
|
|
33
|
+
'sidebar.footer.action': {
|
|
34
|
+
kind: 'list'
|
|
35
|
+
scope: 'root'
|
|
36
|
+
owner: { wide: boolean }
|
|
37
|
+
}
|
|
32
38
|
}
|
|
33
39
|
}
|
|
34
40
|
|
|
@@ -53,4 +59,12 @@ export function apply(ctx: ClientContext): void {
|
|
|
53
59
|
label: () => t('settings.title'),
|
|
54
60
|
locale: NS,
|
|
55
61
|
}, RezSettingsPage))
|
|
62
|
+
|
|
63
|
+
ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({
|
|
64
|
+
name: 'sidebar.footer.action',
|
|
65
|
+
id: 'rez-upgrade',
|
|
66
|
+
order: 10,
|
|
67
|
+
label: () => t('upgrade.label'),
|
|
68
|
+
locale: NS,
|
|
69
|
+
}, UpgradeButton))
|
|
56
70
|
}
|
package/src/client/locales.ts
CHANGED
|
@@ -104,6 +104,13 @@ export const zh = {
|
|
|
104
104
|
'weixin.listening': '通道在听:微信 → 网页「微信」会话(同一条,不另开窗口)→ 回微信。',
|
|
105
105
|
'weixin.loggedIn': '已登录,通道未在听。点「重新连接」即可拉起,不必再扫码。',
|
|
106
106
|
'weixin.loggedOut': '尚未登录。',
|
|
107
|
+
'upgrade.label': '升级',
|
|
108
|
+
'upgrade.current': '已最新',
|
|
109
|
+
'upgrade.aria': '升级 ReZ-TI 插件',
|
|
110
|
+
'upgrade.running': '升级中…',
|
|
111
|
+
'upgrade.reloading': '正在重载',
|
|
112
|
+
'upgrade.already': '已是 {version}',
|
|
113
|
+
'upgrade.reloadTimeout': '升级完成,但网页还没重新连上。请刷新这一页。',
|
|
107
114
|
} as const
|
|
108
115
|
|
|
109
116
|
export type RezKey = keyof typeof zh
|
|
@@ -206,6 +213,13 @@ export const en: Record<RezKey, string> = {
|
|
|
206
213
|
'weixin.listening': 'Channel is live: WeChat → the WeChat folder session on the left → WeChat.',
|
|
207
214
|
'weixin.loggedIn': 'Logged in, listener idle. Click Reconnect — no need to scan again.',
|
|
208
215
|
'weixin.loggedOut': 'Not logged in.',
|
|
216
|
+
'upgrade.label': 'Upgrade',
|
|
217
|
+
'upgrade.current': 'Up to date',
|
|
218
|
+
'upgrade.aria': 'Upgrade the ReZ-TI plugin',
|
|
219
|
+
'upgrade.running': 'Updating…',
|
|
220
|
+
'upgrade.reloading': 'Reloading',
|
|
221
|
+
'upgrade.already': 'Already {version}',
|
|
222
|
+
'upgrade.reloadTimeout': 'Update finished, but the page did not reconnect. Refresh this tab.',
|
|
209
223
|
}
|
|
210
224
|
|
|
211
225
|
/** Minimal {name} interpolator shared by the panel helpers. */
|
|
@@ -294,3 +294,85 @@
|
|
|
294
294
|
font-weight: 600;
|
|
295
295
|
color: var(--dsw-alias-label-primary);
|
|
296
296
|
}
|
|
297
|
+
|
|
298
|
+
.upgradeLayer {
|
|
299
|
+
flex: none;
|
|
300
|
+
align-items: center;
|
|
301
|
+
width: 100%;
|
|
302
|
+
height: 42px;
|
|
303
|
+
margin: 8px 0 0;
|
|
304
|
+
display: flex;
|
|
305
|
+
position: relative;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.upgradeLayerRail {
|
|
309
|
+
width: 36px;
|
|
310
|
+
height: 36px;
|
|
311
|
+
margin: 0;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.upgradeButton {
|
|
315
|
+
width: calc(100% + 4px);
|
|
316
|
+
height: 42px;
|
|
317
|
+
color: var(--dsw-alias-label-primary);
|
|
318
|
+
cursor: pointer;
|
|
319
|
+
background: transparent;
|
|
320
|
+
border: none;
|
|
321
|
+
border-radius: 12px;
|
|
322
|
+
align-items: center;
|
|
323
|
+
gap: 8px;
|
|
324
|
+
margin: 0 -2px;
|
|
325
|
+
padding: 0 10px 0 8px;
|
|
326
|
+
font-family: inherit;
|
|
327
|
+
font-size: 14px;
|
|
328
|
+
display: inline-flex;
|
|
329
|
+
overflow: hidden;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.upgradeButton:hover,
|
|
333
|
+
.upgradeButton[data-outdated] {
|
|
334
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
.upgradeButton:disabled {
|
|
338
|
+
cursor: progress;
|
|
339
|
+
opacity: 0.72;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.upgradeButton:focus-visible {
|
|
343
|
+
outline: 2px solid var(--dsw-alias-brand-primary);
|
|
344
|
+
outline-offset: -2px;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
.upgradeLayerRail .upgradeButton {
|
|
348
|
+
border-radius: 50%;
|
|
349
|
+
justify-content: center;
|
|
350
|
+
gap: 0;
|
|
351
|
+
width: 36px;
|
|
352
|
+
height: 36px;
|
|
353
|
+
padding: 0;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.upgradeLabel {
|
|
357
|
+
text-overflow: ellipsis;
|
|
358
|
+
white-space: nowrap;
|
|
359
|
+
min-width: 0;
|
|
360
|
+
overflow: hidden;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.upgradeMeta {
|
|
364
|
+
color: var(--dsw-alias-label-tertiary);
|
|
365
|
+
font-variant-numeric: tabular-nums;
|
|
366
|
+
flex: none;
|
|
367
|
+
margin-left: auto;
|
|
368
|
+
font-size: 12px;
|
|
369
|
+
line-height: 16px;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
.upgradeDot {
|
|
373
|
+
width: 7px;
|
|
374
|
+
height: 7px;
|
|
375
|
+
border-radius: 50%;
|
|
376
|
+
background: var(--dsw-alias-brand-primary);
|
|
377
|
+
flex: none;
|
|
378
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sidebar footer action: upgrade @rezti/dsh-rez-suite without a terminal.
|
|
3
|
+
* Host code still needs a process respawn; the button does that after pnpm add.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
7
|
+
import { RezApi } from './api.ts'
|
|
8
|
+
import { tt } from './panel/helpers.ts'
|
|
9
|
+
import css from './panel/panel.module.css'
|
|
10
|
+
import type { RezUpdateStatus } from '../protocol.ts'
|
|
11
|
+
|
|
12
|
+
function UpgradeIcon() {
|
|
13
|
+
return (
|
|
14
|
+
<svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true">
|
|
15
|
+
<path
|
|
16
|
+
d="M8 12.5V4.8M8 4.8 4.8 8M8 4.8 11.2 8M3 13.2h10"
|
|
17
|
+
fill="none"
|
|
18
|
+
stroke="currentColor"
|
|
19
|
+
strokeWidth="1.4"
|
|
20
|
+
strokeLinecap="round"
|
|
21
|
+
strokeLinejoin="round"
|
|
22
|
+
/>
|
|
23
|
+
</svg>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function waitUntilHostBack(api: RezApi, timeoutMs = 60_000): Promise<void> {
|
|
28
|
+
const deadline = Date.now() + timeoutMs
|
|
29
|
+
await new Promise((resolve) => setTimeout(resolve, 800))
|
|
30
|
+
while (Date.now() < deadline) {
|
|
31
|
+
try {
|
|
32
|
+
await api.updateStatus()
|
|
33
|
+
return
|
|
34
|
+
} catch {
|
|
35
|
+
await new Promise((resolve) => setTimeout(resolve, 700))
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
throw new Error(tt('upgrade.reloadTimeout'))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function UpgradeButton({ wide }: { wide: boolean }) {
|
|
42
|
+
const api = useMemo(() => new RezApi(), [])
|
|
43
|
+
const [info, setInfo] = useState<RezUpdateStatus>()
|
|
44
|
+
const [busy, setBusy] = useState(false)
|
|
45
|
+
const [note, setNote] = useState('')
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
let cancelled = false
|
|
49
|
+
void api.updateStatus().then((value) => {
|
|
50
|
+
if (!cancelled) setInfo(value)
|
|
51
|
+
}).catch((error: unknown) => {
|
|
52
|
+
if (!cancelled) setNote(error instanceof Error ? error.message : String(error))
|
|
53
|
+
})
|
|
54
|
+
return () => { cancelled = true }
|
|
55
|
+
}, [api])
|
|
56
|
+
|
|
57
|
+
const label = busy
|
|
58
|
+
? tt('upgrade.running')
|
|
59
|
+
: info?.outdated === true
|
|
60
|
+
? tt('upgrade.label')
|
|
61
|
+
: tt('upgrade.current')
|
|
62
|
+
|
|
63
|
+
const meta = note !== ''
|
|
64
|
+
? note
|
|
65
|
+
: info === undefined
|
|
66
|
+
? ''
|
|
67
|
+
: info.outdated
|
|
68
|
+
? info.latest
|
|
69
|
+
: info.installed
|
|
70
|
+
|
|
71
|
+
async function onClick(): Promise<void> {
|
|
72
|
+
if (busy) return
|
|
73
|
+
setBusy(true)
|
|
74
|
+
setNote('')
|
|
75
|
+
try {
|
|
76
|
+
const snapshot = info ?? await api.updateStatus()
|
|
77
|
+
setInfo(snapshot)
|
|
78
|
+
if (!snapshot.outdated) {
|
|
79
|
+
setNote(tt('upgrade.already', { version: snapshot.installed }))
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
const result = await api.applyUpdate()
|
|
83
|
+
setInfo(result)
|
|
84
|
+
if (result.restarting) {
|
|
85
|
+
setNote(tt('upgrade.reloading'))
|
|
86
|
+
await waitUntilHostBack(api)
|
|
87
|
+
window.location.reload()
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
setNote(tt('upgrade.already', { version: result.installed }))
|
|
91
|
+
} catch (error) {
|
|
92
|
+
setNote(error instanceof Error ? error.message : String(error))
|
|
93
|
+
} finally {
|
|
94
|
+
setBusy(false)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<div className={wide ? css.upgradeLayer : `${css.upgradeLayer} ${css.upgradeLayerRail}`}>
|
|
100
|
+
<button
|
|
101
|
+
type="button"
|
|
102
|
+
className={css.upgradeButton}
|
|
103
|
+
data-outdated={info?.outdated === true ? '' : undefined}
|
|
104
|
+
aria-label={tt('upgrade.aria')}
|
|
105
|
+
disabled={busy}
|
|
106
|
+
onClick={() => { void onClick() }}
|
|
107
|
+
>
|
|
108
|
+
<UpgradeIcon />
|
|
109
|
+
{wide && <span className={css.upgradeLabel}>{label}</span>}
|
|
110
|
+
{wide && meta !== '' && <span className={css.upgradeMeta}>{meta}</span>}
|
|
111
|
+
{!wide && info?.outdated === true && <span className={css.upgradeDot} />}
|
|
112
|
+
</button>
|
|
113
|
+
</div>
|
|
114
|
+
)
|
|
115
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -28,6 +28,7 @@ import { TokenManager } from './token-manager.ts'
|
|
|
28
28
|
import { rezConfigTool, rezStatusTool } from './tools.ts'
|
|
29
29
|
import type { RezConfig } from './protocol.ts'
|
|
30
30
|
import { bossGuidanceFor, mountBossDesk } from './boss/mount.ts'
|
|
31
|
+
import { liveSelfUpdate } from './self-update.ts'
|
|
31
32
|
|
|
32
33
|
export const name = 'rez-suite'
|
|
33
34
|
|
|
@@ -146,6 +147,7 @@ export function apply(ctx: Context, config?: RezConfig): void {
|
|
|
146
147
|
const emit = ctx.emit as unknown as (event: string, value: string) => void
|
|
147
148
|
emit('credentials/updated', ref)
|
|
148
149
|
},
|
|
150
|
+
selfUpdate: liveSelfUpdate(),
|
|
149
151
|
})
|
|
150
152
|
|
|
151
153
|
let disposeRoutes: (() => void) | undefined
|
package/src/protocol.ts
CHANGED
|
@@ -161,6 +161,18 @@ export interface ApiErrorBody {
|
|
|
161
161
|
error: string
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
/** Installed vs npm latest for the suite package. */
|
|
165
|
+
export interface RezUpdateStatus {
|
|
166
|
+
package: string
|
|
167
|
+
installed: string
|
|
168
|
+
latest: string
|
|
169
|
+
outdated: boolean
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export interface RezUpdateApplyResult extends RezUpdateStatus {
|
|
173
|
+
restarting: boolean
|
|
174
|
+
}
|
|
175
|
+
|
|
164
176
|
/** Route paths the client calls (shared literals). */
|
|
165
177
|
export const REZ_API_BASE = '/api/dsh-rez-suite' as const
|
|
166
178
|
|
|
@@ -172,4 +184,5 @@ export const REZ_API = {
|
|
|
172
184
|
auditReset: REZ_API_BASE + '/audit/reset',
|
|
173
185
|
weixin: REZ_API_BASE + '/weixin',
|
|
174
186
|
weixinVerify: REZ_API_BASE + '/weixin/verify',
|
|
187
|
+
update: REZ_API_BASE + '/update',
|
|
175
188
|
} as const
|
package/src/routes.ts
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
writeManagedCredential,
|
|
14
14
|
type RezSystem,
|
|
15
15
|
} from '@rezti/dsh-rez-sso'
|
|
16
|
-
import type { RezConfig, RezMcpServerSpec } from './protocol.ts'
|
|
16
|
+
import type { RezConfig, RezMcpServerSpec, RezUpdateApplyResult, RezUpdateStatus } from './protocol.ts'
|
|
17
17
|
import { isRezRoleId, REZ_API } from './protocol.ts'
|
|
18
18
|
import type { McpHost } from './mcp-host.ts'
|
|
19
19
|
import type { TokenManager } from './token-manager.ts'
|
|
@@ -135,11 +135,16 @@ export interface RezRoutesDeps {
|
|
|
135
135
|
host: McpHost
|
|
136
136
|
tokens: TokenManager
|
|
137
137
|
onCredentialWritten?: (ref: string) => void
|
|
138
|
+
selfUpdate?: {
|
|
139
|
+
status: () => Promise<RezUpdateStatus>
|
|
140
|
+
apply: () => Promise<RezUpdateApplyResult>
|
|
141
|
+
relaunch: () => void
|
|
142
|
+
}
|
|
138
143
|
}
|
|
139
144
|
|
|
140
145
|
/** Build every /api/dsh-rez-suite route (exact paths). */
|
|
141
146
|
export function makeRoutes(deps: RezRoutesDeps): WebRoute[] {
|
|
142
|
-
const { getConfig, updateConfig, host, tokens, onCredentialWritten } = deps
|
|
147
|
+
const { getConfig, updateConfig, host, tokens, onCredentialWritten, selfUpdate } = deps
|
|
143
148
|
|
|
144
149
|
const guard = (req: IncomingMessage, res: ServerResponse, method: string): boolean => {
|
|
145
150
|
if (!isLoopbackRequest(req)) {
|
|
@@ -215,5 +220,28 @@ export function makeRoutes(deps: RezRoutesDeps): WebRoute[] {
|
|
|
215
220
|
writeJson(res, 200, { ok: true })
|
|
216
221
|
},
|
|
217
222
|
},
|
|
223
|
+
{
|
|
224
|
+
kind: 'exact',
|
|
225
|
+
path: REZ_API.update,
|
|
226
|
+
handler: async (req, res) => {
|
|
227
|
+
const method = req.method === 'POST' ? 'POST' : 'GET'
|
|
228
|
+
if (!guard(req, res, method)) return
|
|
229
|
+
if (selfUpdate === undefined) {
|
|
230
|
+
writeJson(res, 501, { error: 'self-update is not available' })
|
|
231
|
+
return
|
|
232
|
+
}
|
|
233
|
+
try {
|
|
234
|
+
if (method === 'GET') {
|
|
235
|
+
writeJson(res, 200, await selfUpdate.status())
|
|
236
|
+
return
|
|
237
|
+
}
|
|
238
|
+
const result = await selfUpdate.apply()
|
|
239
|
+
writeJson(res, 200, result)
|
|
240
|
+
if (result.restarting) setTimeout(() => { selfUpdate.relaunch() }, 400)
|
|
241
|
+
} catch (error) {
|
|
242
|
+
writeJson(res, 502, { error: error instanceof Error ? error.message : String(error) })
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
},
|
|
218
246
|
]
|
|
219
247
|
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-place upgrade of @rezti/dsh-rez-suite inside the running dsh profile.
|
|
3
|
+
* pnpm add writes the new bits; Node cannot replace an already-imported host
|
|
4
|
+
* module, so we respawn the same argv after the HTTP response flushes.
|
|
5
|
+
*/
|
|
6
|
+
import { spawn } from 'node:child_process'
|
|
7
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
8
|
+
import { dirname, join } from 'node:path'
|
|
9
|
+
import { fileURLToPath } from 'node:url'
|
|
10
|
+
import type { RezUpdateApplyResult, RezUpdateStatus } from './protocol.ts'
|
|
11
|
+
|
|
12
|
+
export const SUITE_PACKAGE = '@rezti/dsh-rez-suite'
|
|
13
|
+
const NPM_LATEST = 'https://registry.npmjs.org/@rezti/dsh-rez-suite/latest'
|
|
14
|
+
const PNPM_TIMEOUT_MS = 180_000
|
|
15
|
+
|
|
16
|
+
export function compareVersions(left: string, right: string): number {
|
|
17
|
+
const a = left.split('.').map((part) => Number.parseInt(part, 10) || 0)
|
|
18
|
+
const b = right.split('.').map((part) => Number.parseInt(part, 10) || 0)
|
|
19
|
+
const n = Math.max(a.length, b.length)
|
|
20
|
+
for (let i = 0; i < n; i += 1) {
|
|
21
|
+
const av = a[i] ?? 0
|
|
22
|
+
const bv = b[i] ?? 0
|
|
23
|
+
if (av > bv) return 1
|
|
24
|
+
if (av < bv) return -1
|
|
25
|
+
}
|
|
26
|
+
return 0
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function readInstalledVersion(packageDir: string): string {
|
|
30
|
+
const raw = JSON.parse(readFileSync(join(packageDir, 'package.json'), 'utf8')) as { version?: unknown }
|
|
31
|
+
if (typeof raw.version !== 'string' || raw.version === '') throw new Error('suite package.json missing version')
|
|
32
|
+
return raw.version
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function findProfileDir(startDir: string): string {
|
|
36
|
+
let dir = startDir
|
|
37
|
+
for (let i = 0; i < 12; i += 1) {
|
|
38
|
+
const manifest = join(dir, 'package.json')
|
|
39
|
+
if (existsSync(manifest)) {
|
|
40
|
+
try {
|
|
41
|
+
const raw = JSON.parse(readFileSync(manifest, 'utf8')) as { dsh?: { profile?: { bundles?: unknown } } }
|
|
42
|
+
if (Array.isArray(raw.dsh?.profile?.bundles)) return dir
|
|
43
|
+
} catch {
|
|
44
|
+
// keep walking
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const parent = dirname(dir)
|
|
48
|
+
if (parent === dir) break
|
|
49
|
+
dir = parent
|
|
50
|
+
}
|
|
51
|
+
throw new Error('cannot find dsh profile directory (no package.json with dsh.profile.bundles)')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function shellQuote(value: string): string {
|
|
55
|
+
return `'${value.replaceAll("'", `'\\''`)}'`
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function relaunchScript(argv: string[], parentPid: number): string {
|
|
59
|
+
return `while kill -0 ${parentPid} 2>/dev/null; do sleep 0.15; done; sleep 0.25; exec ${argv.map(shellQuote).join(' ')}`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function fetchLatestVersion(fetchImpl: typeof fetch = fetch): Promise<string> {
|
|
63
|
+
const response = await fetchImpl(NPM_LATEST, { headers: { accept: 'application/json' } })
|
|
64
|
+
if (!response.ok) throw new Error(`npm registry HTTP ${response.status}`)
|
|
65
|
+
const body = await response.json() as { version?: unknown }
|
|
66
|
+
if (typeof body.version !== 'string' || body.version === '') throw new Error('npm registry missing version')
|
|
67
|
+
return body.version
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function runPnpmAdd(cwd: string, spec: string, env: NodeJS.ProcessEnv = process.env): Promise<void> {
|
|
71
|
+
return new Promise((resolve, reject) => {
|
|
72
|
+
const child = spawn('pnpm', ['add', spec], { cwd, env, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
73
|
+
let stderr = ''
|
|
74
|
+
child.stderr.on('data', (chunk: Buffer) => { stderr += chunk.toString() })
|
|
75
|
+
const timer = setTimeout(() => {
|
|
76
|
+
child.kill('SIGTERM')
|
|
77
|
+
reject(new Error('pnpm add timed out after 3 minutes'))
|
|
78
|
+
}, PNPM_TIMEOUT_MS)
|
|
79
|
+
child.on('error', (error) => {
|
|
80
|
+
clearTimeout(timer)
|
|
81
|
+
reject(error)
|
|
82
|
+
})
|
|
83
|
+
child.on('close', (code) => {
|
|
84
|
+
clearTimeout(timer)
|
|
85
|
+
if (code === 0) resolve()
|
|
86
|
+
else reject(new Error(stderr.trim() || `pnpm add exited ${String(code)}`))
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function spawnRelaunch(argv: string[], parentPid: number, cwd: string, env: NodeJS.ProcessEnv = process.env): void {
|
|
92
|
+
const child = spawn('/bin/bash', ['-c', relaunchScript(argv, parentPid)], {
|
|
93
|
+
cwd,
|
|
94
|
+
env,
|
|
95
|
+
detached: true,
|
|
96
|
+
stdio: 'ignore',
|
|
97
|
+
})
|
|
98
|
+
child.unref()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface SelfUpdateIo {
|
|
102
|
+
packageDir: string
|
|
103
|
+
fetchLatest: () => Promise<string>
|
|
104
|
+
install: (profileDir: string, spec: string) => Promise<void>
|
|
105
|
+
relaunch: () => void
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let inflight: Promise<unknown> | undefined
|
|
109
|
+
|
|
110
|
+
export function createSelfUpdate(io: SelfUpdateIo) {
|
|
111
|
+
const status = async (): Promise<RezUpdateStatus> => {
|
|
112
|
+
const installed = readInstalledVersion(io.packageDir)
|
|
113
|
+
const latest = await io.fetchLatest()
|
|
114
|
+
return {
|
|
115
|
+
package: SUITE_PACKAGE,
|
|
116
|
+
installed,
|
|
117
|
+
latest,
|
|
118
|
+
outdated: compareVersions(latest, installed) > 0,
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const apply = async (): Promise<RezUpdateApplyResult> => {
|
|
123
|
+
if (inflight !== undefined) throw new Error('upgrade already running')
|
|
124
|
+
const work = (async () => {
|
|
125
|
+
const snapshot = await status()
|
|
126
|
+
if (!snapshot.outdated) {
|
|
127
|
+
return { ...snapshot, restarting: false }
|
|
128
|
+
}
|
|
129
|
+
const profileDir = findProfileDir(io.packageDir)
|
|
130
|
+
await io.install(profileDir, `${SUITE_PACKAGE}@${snapshot.latest}`)
|
|
131
|
+
return { ...snapshot, installed: snapshot.latest, outdated: false, restarting: true }
|
|
132
|
+
})()
|
|
133
|
+
inflight = work
|
|
134
|
+
try {
|
|
135
|
+
return await work
|
|
136
|
+
} finally {
|
|
137
|
+
inflight = undefined
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return { status, apply, relaunch: io.relaunch }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function liveSelfUpdate(): ReturnType<typeof createSelfUpdate> {
|
|
145
|
+
const packageDir = dirname(fileURLToPath(new URL('.', import.meta.url)))
|
|
146
|
+
return createSelfUpdate({
|
|
147
|
+
packageDir,
|
|
148
|
+
fetchLatest: () => fetchLatestVersion(),
|
|
149
|
+
install: (profileDir, spec) => runPnpmAdd(profileDir, spec),
|
|
150
|
+
relaunch: () => {
|
|
151
|
+
spawnRelaunch(process.argv, process.pid, process.cwd())
|
|
152
|
+
setTimeout(() => { process.exit(0) }, 50)
|
|
153
|
+
},
|
|
154
|
+
})
|
|
155
|
+
}
|