@rezti/dsh-rez-suite 0.1.24 → 0.1.26
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/lib/client.d.ts +15 -1
- package/lib/client.js +143 -7
- 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 +16 -3
- 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/lib/client.d.ts
CHANGED
|
@@ -7356,6 +7356,13 @@ declare const zh: {
|
|
|
7356
7356
|
readonly 'weixin.listening': "通道在听:微信 → 网页「微信」会话(同一条,不另开窗口)→ 回微信。";
|
|
7357
7357
|
readonly 'weixin.loggedIn': "已登录,通道未在听。点「重新连接」即可拉起,不必再扫码。";
|
|
7358
7358
|
readonly 'weixin.loggedOut': "尚未登录。";
|
|
7359
|
+
readonly 'upgrade.label': "升级";
|
|
7360
|
+
readonly 'upgrade.current': "已最新";
|
|
7361
|
+
readonly 'upgrade.aria': "升级 ReZ-TI 插件";
|
|
7362
|
+
readonly 'upgrade.running': "升级中…";
|
|
7363
|
+
readonly 'upgrade.reloading': "正在重载";
|
|
7364
|
+
readonly 'upgrade.already': "已是 {version}";
|
|
7365
|
+
readonly 'upgrade.reloadTimeout': "升级完成,但网页还没重新连上。请刷新这一页。";
|
|
7359
7366
|
};
|
|
7360
7367
|
type RezKey = keyof typeof zh;
|
|
7361
7368
|
//#endregion
|
|
@@ -7366,7 +7373,7 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
|
7366
7373
|
}
|
|
7367
7374
|
interface SlotMap {
|
|
7368
7375
|
'settings.plugin.item': {
|
|
7369
|
-
kind: '
|
|
7376
|
+
kind: 'keyed';
|
|
7370
7377
|
scope: 'root';
|
|
7371
7378
|
owner: {
|
|
7372
7379
|
children?: never;
|
|
@@ -7379,6 +7386,13 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
|
7379
7386
|
children?: never;
|
|
7380
7387
|
};
|
|
7381
7388
|
};
|
|
7389
|
+
'sidebar.footer.action': {
|
|
7390
|
+
kind: 'list';
|
|
7391
|
+
scope: 'root';
|
|
7392
|
+
owner: {
|
|
7393
|
+
wide: boolean;
|
|
7394
|
+
};
|
|
7395
|
+
};
|
|
7382
7396
|
}
|
|
7383
7397
|
}
|
|
7384
7398
|
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");
|
|
@@ -116,7 +116,14 @@ const zh = {
|
|
|
116
116
|
"weixin.qrAlt": "微信登录二维码",
|
|
117
117
|
"weixin.listening": "通道在听:微信 → 网页「微信」会话(同一条,不另开窗口)→ 回微信。",
|
|
118
118
|
"weixin.loggedIn": "已登录,通道未在听。点「重新连接」即可拉起,不必再扫码。",
|
|
119
|
-
"weixin.loggedOut": "尚未登录。"
|
|
119
|
+
"weixin.loggedOut": "尚未登录。",
|
|
120
|
+
"upgrade.label": "升级",
|
|
121
|
+
"upgrade.current": "已最新",
|
|
122
|
+
"upgrade.aria": "升级 ReZ-TI 插件",
|
|
123
|
+
"upgrade.running": "升级中…",
|
|
124
|
+
"upgrade.reloading": "正在重载",
|
|
125
|
+
"upgrade.already": "已是 {version}",
|
|
126
|
+
"upgrade.reloadTimeout": "升级完成,但网页还没重新连上。请刷新这一页。"
|
|
120
127
|
};
|
|
121
128
|
const en = {
|
|
122
129
|
"entry.label": "Rez",
|
|
@@ -222,7 +229,14 @@ const en = {
|
|
|
222
229
|
"weixin.qrAlt": "WeChat login QR",
|
|
223
230
|
"weixin.listening": "Channel is live: WeChat → the WeChat folder session on the left → WeChat.",
|
|
224
231
|
"weixin.loggedIn": "Logged in, listener idle. Click Reconnect — no need to scan again.",
|
|
225
|
-
"weixin.loggedOut": "Not logged in."
|
|
232
|
+
"weixin.loggedOut": "Not logged in.",
|
|
233
|
+
"upgrade.label": "Upgrade",
|
|
234
|
+
"upgrade.current": "Up to date",
|
|
235
|
+
"upgrade.aria": "Upgrade the ReZ-TI plugin",
|
|
236
|
+
"upgrade.running": "Updating…",
|
|
237
|
+
"upgrade.reloading": "Reloading",
|
|
238
|
+
"upgrade.already": "Already {version}",
|
|
239
|
+
"upgrade.reloadTimeout": "Update finished, but the page did not reconnect. Refresh this tab."
|
|
226
240
|
};
|
|
227
241
|
/** Minimal {name} interpolator shared by the panel helpers. */
|
|
228
242
|
function t(dict, key, values) {
|
|
@@ -239,7 +253,8 @@ const REZ_API = {
|
|
|
239
253
|
audit: "/api/dsh-rez-suite/audit",
|
|
240
254
|
auditReset: "/api/dsh-rez-suite/audit/reset",
|
|
241
255
|
weixin: "/api/dsh-rez-suite/weixin",
|
|
242
|
-
weixinVerify: "/api/dsh-rez-suite/weixin/verify"
|
|
256
|
+
weixinVerify: "/api/dsh-rez-suite/weixin/verify",
|
|
257
|
+
update: "/api/dsh-rez-suite/update"
|
|
243
258
|
};
|
|
244
259
|
//#endregion
|
|
245
260
|
//#region src/client/api.ts
|
|
@@ -301,6 +316,12 @@ var RezApi = class {
|
|
|
301
316
|
async weixinLogout() {
|
|
302
317
|
return readJson(await fetch(REZ_API.weixin, { method: "DELETE" }));
|
|
303
318
|
}
|
|
319
|
+
async updateStatus() {
|
|
320
|
+
return readJson(await fetch(REZ_API.update));
|
|
321
|
+
}
|
|
322
|
+
async applyUpdate() {
|
|
323
|
+
return readJson(await fetch(REZ_API.update, { method: "POST" }));
|
|
324
|
+
}
|
|
304
325
|
};
|
|
305
326
|
//#endregion
|
|
306
327
|
//#region src/client/panel/helpers.ts
|
|
@@ -355,7 +376,13 @@ var panel_module_default = {
|
|
|
355
376
|
"tabBody": "LOjPTW_tabBody",
|
|
356
377
|
"table": "LOjPTW_table",
|
|
357
378
|
"tableWrap": "LOjPTW_tableWrap",
|
|
358
|
-
"toolbar": "LOjPTW_toolbar"
|
|
379
|
+
"toolbar": "LOjPTW_toolbar",
|
|
380
|
+
"upgradeButton": "LOjPTW_upgradeButton",
|
|
381
|
+
"upgradeDot": "LOjPTW_upgradeDot",
|
|
382
|
+
"upgradeLabel": "LOjPTW_upgradeLabel",
|
|
383
|
+
"upgradeLayer": "LOjPTW_upgradeLayer",
|
|
384
|
+
"upgradeLayerRail": "LOjPTW_upgradeLayerRail",
|
|
385
|
+
"upgradeMeta": "LOjPTW_upgradeMeta"
|
|
359
386
|
};
|
|
360
387
|
//#endregion
|
|
361
388
|
//#region src/client/panel/AuditTab.tsx
|
|
@@ -1333,6 +1360,109 @@ function RezPluginCard() {
|
|
|
1333
1360
|
});
|
|
1334
1361
|
}
|
|
1335
1362
|
//#endregion
|
|
1363
|
+
//#region src/client/upgrade-button.tsx
|
|
1364
|
+
/**
|
|
1365
|
+
* Sidebar footer action: upgrade @rezti/dsh-rez-suite without a terminal.
|
|
1366
|
+
* Host code still needs a process respawn; the button does that after pnpm add.
|
|
1367
|
+
*/
|
|
1368
|
+
function UpgradeIcon() {
|
|
1369
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
1370
|
+
width: "16",
|
|
1371
|
+
height: "16",
|
|
1372
|
+
viewBox: "0 0 16 16",
|
|
1373
|
+
"aria-hidden": "true",
|
|
1374
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
1375
|
+
d: "M8 12.5V4.8M8 4.8 4.8 8M8 4.8 11.2 8M3 13.2h10",
|
|
1376
|
+
fill: "none",
|
|
1377
|
+
stroke: "currentColor",
|
|
1378
|
+
strokeWidth: "1.4",
|
|
1379
|
+
strokeLinecap: "round",
|
|
1380
|
+
strokeLinejoin: "round"
|
|
1381
|
+
})
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1384
|
+
async function waitUntilHostBack(api, timeoutMs = 6e4) {
|
|
1385
|
+
const deadline = Date.now() + timeoutMs;
|
|
1386
|
+
await new Promise((resolve) => setTimeout(resolve, 800));
|
|
1387
|
+
while (Date.now() < deadline) try {
|
|
1388
|
+
await api.updateStatus();
|
|
1389
|
+
return;
|
|
1390
|
+
} catch {
|
|
1391
|
+
await new Promise((resolve) => setTimeout(resolve, 700));
|
|
1392
|
+
}
|
|
1393
|
+
throw new Error(tt("upgrade.reloadTimeout"));
|
|
1394
|
+
}
|
|
1395
|
+
function UpgradeButton({ wide }) {
|
|
1396
|
+
const api = (0, react.useMemo)(() => new RezApi(), []);
|
|
1397
|
+
const [info, setInfo] = (0, react.useState)();
|
|
1398
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
1399
|
+
const [note, setNote] = (0, react.useState)("");
|
|
1400
|
+
(0, react.useEffect)(() => {
|
|
1401
|
+
let cancelled = false;
|
|
1402
|
+
api.updateStatus().then((value) => {
|
|
1403
|
+
if (!cancelled) setInfo(value);
|
|
1404
|
+
}).catch((error) => {
|
|
1405
|
+
if (!cancelled) setNote(error instanceof Error ? error.message : String(error));
|
|
1406
|
+
});
|
|
1407
|
+
return () => {
|
|
1408
|
+
cancelled = true;
|
|
1409
|
+
};
|
|
1410
|
+
}, [api]);
|
|
1411
|
+
const label = busy ? tt("upgrade.running") : info?.outdated === true ? tt("upgrade.label") : tt("upgrade.current");
|
|
1412
|
+
const meta = note !== "" ? note : info === void 0 ? "" : info.outdated ? info.latest : info.installed;
|
|
1413
|
+
async function onClick() {
|
|
1414
|
+
if (busy) return;
|
|
1415
|
+
setBusy(true);
|
|
1416
|
+
setNote("");
|
|
1417
|
+
try {
|
|
1418
|
+
const snapshot = info ?? await api.updateStatus();
|
|
1419
|
+
setInfo(snapshot);
|
|
1420
|
+
if (!snapshot.outdated) {
|
|
1421
|
+
setNote(tt("upgrade.already", { version: snapshot.installed }));
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1424
|
+
const result = await api.applyUpdate();
|
|
1425
|
+
setInfo(result);
|
|
1426
|
+
if (result.restarting) {
|
|
1427
|
+
setNote(tt("upgrade.reloading"));
|
|
1428
|
+
await waitUntilHostBack(api);
|
|
1429
|
+
window.location.reload();
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
setNote(tt("upgrade.already", { version: result.installed }));
|
|
1433
|
+
} catch (error) {
|
|
1434
|
+
setNote(error instanceof Error ? error.message : String(error));
|
|
1435
|
+
} finally {
|
|
1436
|
+
setBusy(false);
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1440
|
+
className: wide ? panel_module_default.upgradeLayer : `${panel_module_default.upgradeLayer} ${panel_module_default.upgradeLayerRail}`,
|
|
1441
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1442
|
+
type: "button",
|
|
1443
|
+
className: panel_module_default.upgradeButton,
|
|
1444
|
+
"data-outdated": info?.outdated === true ? "" : void 0,
|
|
1445
|
+
"aria-label": tt("upgrade.aria"),
|
|
1446
|
+
disabled: busy,
|
|
1447
|
+
onClick: () => {
|
|
1448
|
+
onClick();
|
|
1449
|
+
},
|
|
1450
|
+
children: [
|
|
1451
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UpgradeIcon, {}),
|
|
1452
|
+
wide && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1453
|
+
className: panel_module_default.upgradeLabel,
|
|
1454
|
+
children: label
|
|
1455
|
+
}),
|
|
1456
|
+
wide && meta !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1457
|
+
className: panel_module_default.upgradeMeta,
|
|
1458
|
+
children: meta
|
|
1459
|
+
}),
|
|
1460
|
+
!wide && info?.outdated === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: panel_module_default.upgradeDot })
|
|
1461
|
+
]
|
|
1462
|
+
})
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
//#endregion
|
|
1336
1466
|
//#region src/client/index.ts
|
|
1337
1467
|
const NS = "dsh-rez-suite";
|
|
1338
1468
|
const inject = ["slots", "locale"];
|
|
@@ -1344,8 +1474,7 @@ function apply(ctx) {
|
|
|
1344
1474
|
}), "dsh-rez-suite: dictionaries");
|
|
1345
1475
|
ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({
|
|
1346
1476
|
name: "settings.plugin.item",
|
|
1347
|
-
|
|
1348
|
-
order: 40,
|
|
1477
|
+
key: NS,
|
|
1349
1478
|
locale: NS
|
|
1350
1479
|
}, RezPluginCard));
|
|
1351
1480
|
ctx.slots.inject("settings.plugins.tab", () => ctx.slots.register({
|
|
@@ -1355,6 +1484,13 @@ function apply(ctx) {
|
|
|
1355
1484
|
label: () => t("settings.title"),
|
|
1356
1485
|
locale: NS
|
|
1357
1486
|
}, RezSettingsPage));
|
|
1487
|
+
ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
|
|
1488
|
+
name: "sidebar.footer.action",
|
|
1489
|
+
id: "rez-upgrade",
|
|
1490
|
+
order: 10,
|
|
1491
|
+
label: () => t("upgrade.label"),
|
|
1492
|
+
locale: NS
|
|
1493
|
+
}, UpgradeButton));
|
|
1358
1494
|
}
|
|
1359
1495
|
//#endregion
|
|
1360
1496
|
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) {
|
|
@@ -2957,7 +2958,8 @@ const REZ_API = {
|
|
|
2957
2958
|
audit: "/api/dsh-rez-suite/audit",
|
|
2958
2959
|
auditReset: "/api/dsh-rez-suite/audit/reset",
|
|
2959
2960
|
weixin: "/api/dsh-rez-suite/weixin",
|
|
2960
|
-
weixinVerify: "/api/dsh-rez-suite/weixin/verify"
|
|
2961
|
+
weixinVerify: "/api/dsh-rez-suite/weixin/verify",
|
|
2962
|
+
update: "/api/dsh-rez-suite/update"
|
|
2961
2963
|
};
|
|
2962
2964
|
//#endregion
|
|
2963
2965
|
//#region src/store.ts
|
|
@@ -3220,7 +3222,7 @@ function applyPublicPatch(current, patch) {
|
|
|
3220
3222
|
}
|
|
3221
3223
|
/** Build every /api/dsh-rez-suite route (exact paths). */
|
|
3222
3224
|
function makeRoutes(deps) {
|
|
3223
|
-
const { getConfig, updateConfig, host, tokens, onCredentialWritten } = deps;
|
|
3225
|
+
const { getConfig, updateConfig, host, tokens, onCredentialWritten, selfUpdate } = deps;
|
|
3224
3226
|
const guard = (req, res, method) => {
|
|
3225
3227
|
if (!isLoopbackRequest(req)) {
|
|
3226
3228
|
writeJson(res, 403, { error: "forbidden: loopback-only" });
|
|
@@ -3292,6 +3294,31 @@ function makeRoutes(deps) {
|
|
|
3292
3294
|
tokens.reset();
|
|
3293
3295
|
writeJson(res, 200, { ok: true });
|
|
3294
3296
|
}
|
|
3297
|
+
},
|
|
3298
|
+
{
|
|
3299
|
+
kind: "exact",
|
|
3300
|
+
path: REZ_API.update,
|
|
3301
|
+
handler: async (req, res) => {
|
|
3302
|
+
const method = req.method === "POST" ? "POST" : "GET";
|
|
3303
|
+
if (!guard(req, res, method)) return;
|
|
3304
|
+
if (selfUpdate === void 0) {
|
|
3305
|
+
writeJson(res, 501, { error: "self-update is not available" });
|
|
3306
|
+
return;
|
|
3307
|
+
}
|
|
3308
|
+
try {
|
|
3309
|
+
if (method === "GET") {
|
|
3310
|
+
writeJson(res, 200, await selfUpdate.status());
|
|
3311
|
+
return;
|
|
3312
|
+
}
|
|
3313
|
+
const result = await selfUpdate.apply();
|
|
3314
|
+
writeJson(res, 200, result);
|
|
3315
|
+
if (result.restarting) setTimeout(() => {
|
|
3316
|
+
selfUpdate.relaunch();
|
|
3317
|
+
}, 400);
|
|
3318
|
+
} catch (error) {
|
|
3319
|
+
writeJson(res, 502, { error: error instanceof Error ? error.message : String(error) });
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3295
3322
|
}
|
|
3296
3323
|
];
|
|
3297
3324
|
}
|
|
@@ -7906,6 +7933,153 @@ function mountRezWorkspaces(ctx, getRole) {
|
|
|
7906
7933
|
return run;
|
|
7907
7934
|
}
|
|
7908
7935
|
//#endregion
|
|
7936
|
+
//#region src/self-update.ts
|
|
7937
|
+
/**
|
|
7938
|
+
* In-place upgrade of @rezti/dsh-rez-suite inside the running dsh profile.
|
|
7939
|
+
* pnpm add writes the new bits; Node cannot replace an already-imported host
|
|
7940
|
+
* module, so we respawn the same argv after the HTTP response flushes.
|
|
7941
|
+
*/
|
|
7942
|
+
const SUITE_PACKAGE = "@rezti/dsh-rez-suite";
|
|
7943
|
+
const NPM_LATEST = "https://registry.npmjs.org/@rezti/dsh-rez-suite/latest";
|
|
7944
|
+
const PNPM_TIMEOUT_MS = 18e4;
|
|
7945
|
+
function compareVersions(left, right) {
|
|
7946
|
+
const a = left.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
7947
|
+
const b = right.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
7948
|
+
const n = Math.max(a.length, b.length);
|
|
7949
|
+
for (let i = 0; i < n; i += 1) {
|
|
7950
|
+
const av = a[i] ?? 0;
|
|
7951
|
+
const bv = b[i] ?? 0;
|
|
7952
|
+
if (av > bv) return 1;
|
|
7953
|
+
if (av < bv) return -1;
|
|
7954
|
+
}
|
|
7955
|
+
return 0;
|
|
7956
|
+
}
|
|
7957
|
+
function readInstalledVersion(packageDir) {
|
|
7958
|
+
const raw = JSON.parse(readFileSync(join(packageDir, "package.json"), "utf8"));
|
|
7959
|
+
if (typeof raw.version !== "string" || raw.version === "") throw new Error("suite package.json missing version");
|
|
7960
|
+
return raw.version;
|
|
7961
|
+
}
|
|
7962
|
+
function findProfileDir(startDir) {
|
|
7963
|
+
let dir = startDir;
|
|
7964
|
+
for (let i = 0; i < 12; i += 1) {
|
|
7965
|
+
const manifest = join(dir, "package.json");
|
|
7966
|
+
if (existsSync(manifest)) try {
|
|
7967
|
+
const raw = JSON.parse(readFileSync(manifest, "utf8"));
|
|
7968
|
+
if (Array.isArray(raw.dsh?.profile?.bundles)) return dir;
|
|
7969
|
+
} catch {}
|
|
7970
|
+
const parent = dirname(dir);
|
|
7971
|
+
if (parent === dir) break;
|
|
7972
|
+
dir = parent;
|
|
7973
|
+
}
|
|
7974
|
+
throw new Error("cannot find dsh profile directory (no package.json with dsh.profile.bundles)");
|
|
7975
|
+
}
|
|
7976
|
+
function shellQuote(value) {
|
|
7977
|
+
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
7978
|
+
}
|
|
7979
|
+
function relaunchScript(argv, parentPid) {
|
|
7980
|
+
return `while kill -0 ${parentPid} 2>/dev/null; do sleep 0.15; done; sleep 0.25; exec ${argv.map(shellQuote).join(" ")}`;
|
|
7981
|
+
}
|
|
7982
|
+
async function fetchLatestVersion(fetchImpl = fetch) {
|
|
7983
|
+
const response = await fetchImpl(NPM_LATEST, { headers: { accept: "application/json" } });
|
|
7984
|
+
if (!response.ok) throw new Error(`npm registry HTTP ${response.status}`);
|
|
7985
|
+
const body = await response.json();
|
|
7986
|
+
if (typeof body.version !== "string" || body.version === "") throw new Error("npm registry missing version");
|
|
7987
|
+
return body.version;
|
|
7988
|
+
}
|
|
7989
|
+
function runPnpmAdd(cwd, spec, env = process.env) {
|
|
7990
|
+
return new Promise((resolve, reject) => {
|
|
7991
|
+
const child = spawn("pnpm", ["add", spec], {
|
|
7992
|
+
cwd,
|
|
7993
|
+
env,
|
|
7994
|
+
stdio: [
|
|
7995
|
+
"ignore",
|
|
7996
|
+
"pipe",
|
|
7997
|
+
"pipe"
|
|
7998
|
+
]
|
|
7999
|
+
});
|
|
8000
|
+
let stderr = "";
|
|
8001
|
+
child.stderr.on("data", (chunk) => {
|
|
8002
|
+
stderr += chunk.toString();
|
|
8003
|
+
});
|
|
8004
|
+
const timer = setTimeout(() => {
|
|
8005
|
+
child.kill("SIGTERM");
|
|
8006
|
+
reject(/* @__PURE__ */ new Error("pnpm add timed out after 3 minutes"));
|
|
8007
|
+
}, PNPM_TIMEOUT_MS);
|
|
8008
|
+
child.on("error", (error) => {
|
|
8009
|
+
clearTimeout(timer);
|
|
8010
|
+
reject(error);
|
|
8011
|
+
});
|
|
8012
|
+
child.on("close", (code) => {
|
|
8013
|
+
clearTimeout(timer);
|
|
8014
|
+
if (code === 0) resolve();
|
|
8015
|
+
else reject(new Error(stderr.trim() || `pnpm add exited ${String(code)}`));
|
|
8016
|
+
});
|
|
8017
|
+
});
|
|
8018
|
+
}
|
|
8019
|
+
function spawnRelaunch(argv, parentPid, cwd, env = process.env) {
|
|
8020
|
+
spawn("/bin/bash", ["-c", relaunchScript(argv, parentPid)], {
|
|
8021
|
+
cwd,
|
|
8022
|
+
env,
|
|
8023
|
+
detached: true,
|
|
8024
|
+
stdio: "ignore"
|
|
8025
|
+
}).unref();
|
|
8026
|
+
}
|
|
8027
|
+
let inflight;
|
|
8028
|
+
function createSelfUpdate(io) {
|
|
8029
|
+
const status = async () => {
|
|
8030
|
+
const installed = readInstalledVersion(io.packageDir);
|
|
8031
|
+
const latest = await io.fetchLatest();
|
|
8032
|
+
return {
|
|
8033
|
+
package: SUITE_PACKAGE,
|
|
8034
|
+
installed,
|
|
8035
|
+
latest,
|
|
8036
|
+
outdated: compareVersions(latest, installed) > 0
|
|
8037
|
+
};
|
|
8038
|
+
};
|
|
8039
|
+
const apply = async () => {
|
|
8040
|
+
if (inflight !== void 0) throw new Error("upgrade already running");
|
|
8041
|
+
const work = (async () => {
|
|
8042
|
+
const snapshot = await status();
|
|
8043
|
+
if (!snapshot.outdated) return {
|
|
8044
|
+
...snapshot,
|
|
8045
|
+
restarting: false
|
|
8046
|
+
};
|
|
8047
|
+
const profileDir = findProfileDir(io.packageDir);
|
|
8048
|
+
await io.install(profileDir, `${SUITE_PACKAGE}@${snapshot.latest}`);
|
|
8049
|
+
return {
|
|
8050
|
+
...snapshot,
|
|
8051
|
+
installed: snapshot.latest,
|
|
8052
|
+
outdated: false,
|
|
8053
|
+
restarting: true
|
|
8054
|
+
};
|
|
8055
|
+
})();
|
|
8056
|
+
inflight = work;
|
|
8057
|
+
try {
|
|
8058
|
+
return await work;
|
|
8059
|
+
} finally {
|
|
8060
|
+
inflight = void 0;
|
|
8061
|
+
}
|
|
8062
|
+
};
|
|
8063
|
+
return {
|
|
8064
|
+
status,
|
|
8065
|
+
apply,
|
|
8066
|
+
relaunch: io.relaunch
|
|
8067
|
+
};
|
|
8068
|
+
}
|
|
8069
|
+
function liveSelfUpdate() {
|
|
8070
|
+
return createSelfUpdate({
|
|
8071
|
+
packageDir: dirname(fileURLToPath(new URL(".", import.meta.url))),
|
|
8072
|
+
fetchLatest: () => fetchLatestVersion(),
|
|
8073
|
+
install: (profileDir, spec) => runPnpmAdd(profileDir, spec),
|
|
8074
|
+
relaunch: () => {
|
|
8075
|
+
spawnRelaunch(process.argv, process.pid, process.cwd());
|
|
8076
|
+
setTimeout(() => {
|
|
8077
|
+
process.exit(0);
|
|
8078
|
+
}, 50);
|
|
8079
|
+
}
|
|
8080
|
+
});
|
|
8081
|
+
}
|
|
8082
|
+
//#endregion
|
|
7909
8083
|
//#region src/index.ts
|
|
7910
8084
|
const name = "rez-suite";
|
|
7911
8085
|
const inject = [
|
|
@@ -8032,7 +8206,8 @@ function apply(ctx, config) {
|
|
|
8032
8206
|
onCredentialWritten: (ref) => {
|
|
8033
8207
|
const emit = ctx.emit;
|
|
8034
8208
|
emit("credentials/updated", ref);
|
|
8035
|
-
}
|
|
8209
|
+
},
|
|
8210
|
+
selfUpdate: liveSelfUpdate()
|
|
8036
8211
|
});
|
|
8037
8212
|
let disposeRoutes;
|
|
8038
8213
|
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.26",
|
|
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,15 +39,15 @@
|
|
|
38
39
|
"dependencies": {
|
|
39
40
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
40
41
|
"zod": "^4.4.3",
|
|
41
|
-
"@rezti/dsh-rez-ha-bridge": "0.1.9",
|
|
42
|
-
"@rezti/dsh-rez-gsc": "0.1.0",
|
|
43
|
-
"@rezti/dsh-rez-odoo": "0.1.9",
|
|
44
42
|
"@rezti/dsh-rez-intent": "0.1.5",
|
|
43
|
+
"@rezti/dsh-rez-nextcloud": "0.1.9",
|
|
44
|
+
"@rezti/dsh-rez-odoo": "0.1.9",
|
|
45
45
|
"@rezti/dsh-rez-sso": "0.1.9",
|
|
46
46
|
"@rezti/dsh-rez-tapd": "0.1.0",
|
|
47
47
|
"@rezti/dsh-rez-token-manager": "0.1.4",
|
|
48
|
-
"@rezti/dsh-rez-
|
|
49
|
-
"@rezti/dsh-rez-wechat": "0.1.14"
|
|
48
|
+
"@rezti/dsh-rez-ha-bridge": "0.1.9",
|
|
49
|
+
"@rezti/dsh-rez-wechat": "0.1.14",
|
|
50
|
+
"@rezti/dsh-rez-gsc": "0.1.0"
|
|
50
51
|
},
|
|
51
52
|
"peerDependencies": {
|
|
52
53
|
"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
|
|
|
@@ -20,7 +21,7 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
|
20
21
|
}
|
|
21
22
|
interface SlotMap {
|
|
22
23
|
'settings.plugin.item': {
|
|
23
|
-
kind: '
|
|
24
|
+
kind: 'keyed'
|
|
24
25
|
scope: 'root'
|
|
25
26
|
owner: { children?: never }
|
|
26
27
|
}
|
|
@@ -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
|
|
|
@@ -42,8 +48,7 @@ export function apply(ctx: ClientContext): void {
|
|
|
42
48
|
|
|
43
49
|
ctx.slots.inject('settings.plugin.item', () => ctx.slots.register({
|
|
44
50
|
name: 'settings.plugin.item',
|
|
45
|
-
|
|
46
|
-
order: 40,
|
|
51
|
+
key: NS,
|
|
47
52
|
locale: NS,
|
|
48
53
|
}, RezPluginCard))
|
|
49
54
|
|
|
@@ -54,4 +59,12 @@ export function apply(ctx: ClientContext): void {
|
|
|
54
59
|
label: () => t('settings.title'),
|
|
55
60
|
locale: NS,
|
|
56
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))
|
|
57
70
|
}
|
package/src/client/locales.ts
CHANGED
|
@@ -111,6 +111,13 @@ export const zh = {
|
|
|
111
111
|
'weixin.listening': '通道在听:微信 → 网页「微信」会话(同一条,不另开窗口)→ 回微信。',
|
|
112
112
|
'weixin.loggedIn': '已登录,通道未在听。点「重新连接」即可拉起,不必再扫码。',
|
|
113
113
|
'weixin.loggedOut': '尚未登录。',
|
|
114
|
+
'upgrade.label': '升级',
|
|
115
|
+
'upgrade.current': '已最新',
|
|
116
|
+
'upgrade.aria': '升级 ReZ-TI 插件',
|
|
117
|
+
'upgrade.running': '升级中…',
|
|
118
|
+
'upgrade.reloading': '正在重载',
|
|
119
|
+
'upgrade.already': '已是 {version}',
|
|
120
|
+
'upgrade.reloadTimeout': '升级完成,但网页还没重新连上。请刷新这一页。',
|
|
114
121
|
} as const
|
|
115
122
|
|
|
116
123
|
export type RezKey = keyof typeof zh
|
|
@@ -220,6 +227,13 @@ export const en: Record<RezKey, string> = {
|
|
|
220
227
|
'weixin.listening': 'Channel is live: WeChat → the WeChat folder session on the left → WeChat.',
|
|
221
228
|
'weixin.loggedIn': 'Logged in, listener idle. Click Reconnect — no need to scan again.',
|
|
222
229
|
'weixin.loggedOut': 'Not logged in.',
|
|
230
|
+
'upgrade.label': 'Upgrade',
|
|
231
|
+
'upgrade.current': 'Up to date',
|
|
232
|
+
'upgrade.aria': 'Upgrade the ReZ-TI plugin',
|
|
233
|
+
'upgrade.running': 'Updating…',
|
|
234
|
+
'upgrade.reloading': 'Reloading',
|
|
235
|
+
'upgrade.already': 'Already {version}',
|
|
236
|
+
'upgrade.reloadTimeout': 'Update finished, but the page did not reconnect. Refresh this tab.',
|
|
223
237
|
}
|
|
224
238
|
|
|
225
239
|
/** 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 { workspaceGuidanceFor, mountRezWorkspaces } from './boss/mount.ts'
|
|
31
|
+
import { liveSelfUpdate } from './self-update.ts'
|
|
31
32
|
|
|
32
33
|
export const name = 'rez-suite'
|
|
33
34
|
|
|
@@ -158,6 +159,7 @@ export function apply(ctx: Context, config?: RezConfig): void {
|
|
|
158
159
|
const emit = ctx.emit as unknown as (event: string, value: string) => void
|
|
159
160
|
emit('credentials/updated', ref)
|
|
160
161
|
},
|
|
162
|
+
selfUpdate: liveSelfUpdate(),
|
|
161
163
|
})
|
|
162
164
|
|
|
163
165
|
let disposeRoutes: (() => void) | undefined
|
package/src/protocol.ts
CHANGED
|
@@ -168,6 +168,18 @@ export interface ApiErrorBody {
|
|
|
168
168
|
error: string
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
/** Installed vs npm latest for the suite package. */
|
|
172
|
+
export interface RezUpdateStatus {
|
|
173
|
+
package: string
|
|
174
|
+
installed: string
|
|
175
|
+
latest: string
|
|
176
|
+
outdated: boolean
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export interface RezUpdateApplyResult extends RezUpdateStatus {
|
|
180
|
+
restarting: boolean
|
|
181
|
+
}
|
|
182
|
+
|
|
171
183
|
/** Route paths the client calls (shared literals). */
|
|
172
184
|
export const REZ_API_BASE = '/api/dsh-rez-suite' as const
|
|
173
185
|
|
|
@@ -179,4 +191,5 @@ export const REZ_API = {
|
|
|
179
191
|
auditReset: REZ_API_BASE + '/audit/reset',
|
|
180
192
|
weixin: REZ_API_BASE + '/weixin',
|
|
181
193
|
weixinVerify: REZ_API_BASE + '/weixin/verify',
|
|
194
|
+
update: REZ_API_BASE + '/update',
|
|
182
195
|
} 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
|
+
}
|