@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 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: 'list';
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
- id: "rez-suite",
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.24",
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-nextcloud": "0.1.9",
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
  }
@@ -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: 'list'
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
- id: 'rez-suite',
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
  }
@@ -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
+ }