@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 CHANGED
@@ -6,7 +6,7 @@
6
6
  # 第一次
7
7
  dsh plugin --profile web add @rezti/dsh-rez-suite
8
8
 
9
- # 以后升级(只记这一条)
9
+ # 以后升级:侧栏底部「升级」按钮,或
10
10
  dsh plugin --profile web update @rezti/dsh-rez-suite
11
11
  ```
12
12
 
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.19",
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-ha-bridge": "0.1.7",
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-token-manager": "0.1.4"
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
  }
@@ -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
  }
@@ -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
+ }