@linxin666/dsh-ssh 0.2.7 → 0.2.8

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/index.js CHANGED
@@ -2,7 +2,7 @@ import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-sett
2
2
  import { closeSync, createReadStream, createWriteStream, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from "node:fs";
3
3
  import { Client } from "ssh2";
4
4
  import { homedir, tmpdir } from "node:os";
5
- import { basename, dirname, join, relative, resolve } from "node:path";
5
+ import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
6
6
  import { createServer } from "node:net";
7
7
  import { unlink } from "node:fs/promises";
8
8
  import { randomBytes } from "node:crypto";
@@ -412,7 +412,7 @@ var require_lib$1 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
412
412
  });
413
413
  }));
414
414
  //#endregion
415
- //#region src/store.ts
415
+ //#region src/dsh-home.ts
416
416
  var import_lib = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
417
417
  var __defProp = Object.defineProperty;
418
418
  var __name = (target, value) => __defProp(target, "name", {
@@ -1025,7 +1025,39 @@ var import_lib = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((export
1025
1025
  module.exports = Schema;
1026
1026
  })))(), 1);
1027
1027
  /**
1028
- * Host config store: one JSON file (`~/.dsh/dsh-ssh.json`) holding every
1028
+ * DSH_HOME resolution shared by the plugin family's Host halves: the
1029
+ * environment override wins, the platform home fallback follows. Mirrors
1030
+ * what dsh-pet and dsh-liangshen each used to implement locally.
1031
+ */
1032
+ /** Expand a leading ~ (or ~user) in a path, platform-style. */
1033
+ function expandHome$1(path, home = homedir()) {
1034
+ if (path === "~") return home;
1035
+ if (path.startsWith("~/") || path.startsWith("~\\")) return join(home, path.slice(2));
1036
+ return path;
1037
+ }
1038
+ /**
1039
+ * Resolve the DSH home directory.
1040
+ * @param env - process environment to read DSH_HOME from.
1041
+ * @param home - platform home directory fallback (test seam).
1042
+ * @returns the absolute DSH home path.
1043
+ */
1044
+ function resolveDshHome(env = process.env, home = homedir()) {
1045
+ const raw = env.DSH_HOME;
1046
+ if (raw !== void 0 && raw.trim() !== "") {
1047
+ const expanded = expandHome$1(raw.trim(), home);
1048
+ return isAbsolute(expanded) ? expanded : join(process.cwd(), expanded);
1049
+ }
1050
+ return join(home, ".dsh");
1051
+ }
1052
+ /** Resolve the DSH home directory from the live environment. */
1053
+ function dshHome() {
1054
+ return resolveDshHome();
1055
+ }
1056
+ //#endregion
1057
+ //#region src/store.ts
1058
+ /**
1059
+ * Host config store: one JSON file (`$DSH_HOME/dsh-ssh.json`, defaulting
1060
+ * to `~/.dsh`) holding every
1029
1061
  * SSH host entry, written atomically (tmp + rename). Also parses the user's
1030
1062
  * standard `~/.ssh/config` for one-shot import. Secrets (passwords,
1031
1063
  * passphrases) live in this user-owned file in plaintext — same trust model
@@ -1033,9 +1065,9 @@ var import_lib = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((export
1033
1065
  */
1034
1066
  /** File format version. */
1035
1067
  const FORMAT_VERSION = 1;
1036
- /** Store file location: <home>/.dsh/dsh-ssh.json. */
1068
+ /** Store file location: $DSH_HOME/dsh-ssh.json (defaults to ~/.dsh). */
1037
1069
  function storePath() {
1038
- return join(homedir(), ".dsh", "dsh-ssh.json");
1070
+ return join(dshHome(), "dsh-ssh.json");
1039
1071
  }
1040
1072
  /** The user's standard OpenSSH config path. */
1041
1073
  function sshConfigPath() {
@@ -3457,7 +3489,7 @@ const DEFAULT_ANNOUNCE = false;
3457
3489
  /** Order of the announcement section within the tool-guidance band. */
3458
3490
  const SECTION_ORDER = 150;
3459
3491
  /** Model-facing announcement: plugin presence, capabilities, and limits. */
3460
- const SSH_GUIDANCE = "本机已安装 dsh-ssh 插件(DSH 远程 SSH 运维):侧边栏「SSH」入口;在 dsh-web-ui 插件全家桶仓库(packages/dsh-ssh)统一维护。能力:主机配置存 ~/.dsh/dsh-ssh.json(可从 ~/.ssh/config 导入);持久连接池复用长连接(空闲 30 分钟自动断开);ssh_list 列出主机、ssh_exec 执行远程命令、ssh_upload/ssh_download 传输文件、ssh_tunnel 本地端口转发(访问远程数据库/内网服务)、ssh_cluster 集群并发执行;支持密钥/密码/ssh-agent 认证、passphrase 密钥与 ProxyJump 跳板机;Web 终端走 WebSocket。限制:主机操作由用户在 GUI 中配置后 agent 方可使用;密码以明文存在用户主目录私有文件(权限 0600);命令输出原样返回、可能含敏感信息;断线重连可能重放非幂等命令;传输/执行消耗真实远程资源,先确认再操作。用户提到「SSH / 远程服务器 / 服务器操作 / 跳板机 / 隧道 / 部署 / 上传下载」时即指本插件,请据此协作。";
3492
+ const SSH_GUIDANCE = "本机已安装 dsh-ssh 插件(DSH 远程 SSH 运维):侧边栏「SSH」入口;在 dsh-web-ui 插件全家桶仓库(packages/dsh-ssh)统一维护。能力:主机配置存 $DSH_HOME/dsh-ssh.json(默认 ~/.dsh)(可从 ~/.ssh/config 导入);持久连接池复用长连接(空闲 30 分钟自动断开);ssh_list 列出主机、ssh_exec 执行远程命令、ssh_upload/ssh_download 传输文件、ssh_tunnel 本地端口转发(访问远程数据库/内网服务)、ssh_cluster 集群并发执行;支持密钥/密码/ssh-agent 认证、passphrase 密钥与 ProxyJump 跳板机;Web 终端走 WebSocket。限制:主机操作由用户在 GUI 中配置后 agent 方可使用;密码以明文存在用户主目录私有文件(权限 0600);命令输出原样返回、可能含敏感信息;断线重连可能重放非幂等命令;传输/执行消耗真实远程资源,先确认再操作。用户提到「SSH / 远程服务器 / 服务器操作 / 跳板机 / 隧道 / 部署 / 上传下载」时即指本插件,请据此协作。";
3461
3493
  /**
3462
3494
  * Mount the SSH engine, routes, tools, and announcement.
3463
3495
  * @param ctx - host plugin context carrying webServer/tools/systemPrompt.
@@ -59,7 +59,7 @@ export declare const zh: {
59
59
  readonly 'form.keyPathHint': "如 ~/.ssh/id_ed25519";
60
60
  readonly 'form.passphrase': "密钥口令";
61
61
  readonly 'form.password': "密码";
62
- readonly 'form.passwordHint': "明文保存在 ~/.dsh/dsh-ssh.json(本机私有文件)";
62
+ readonly 'form.passwordHint': "明文保存在 $DSH_HOME/dsh-ssh.json(本机私有文件)";
63
63
  readonly 'form.passwordRequired': "密码认证需要填写密码。";
64
64
  readonly 'form.proxyJump': "跳板机别名";
65
65
  readonly 'form.proxyJumpHint': "逗号分隔的本地主机别名,按顺序连接";
@@ -0,0 +1,16 @@
1
+ /**
2
+ * DSH_HOME resolution shared by the plugin family's Host halves: the
3
+ * environment override wins, the platform home fallback follows. Mirrors
4
+ * what dsh-pet and dsh-liangshen each used to implement locally.
5
+ */
6
+ /** Expand a leading ~ (or ~user) in a path, platform-style. */
7
+ export declare function expandHome(path: string, home?: string): string;
8
+ /**
9
+ * Resolve the DSH home directory.
10
+ * @param env - process environment to read DSH_HOME from.
11
+ * @param home - platform home directory fallback (test seam).
12
+ * @returns the absolute DSH home path.
13
+ */
14
+ export declare function resolveDshHome(env?: NodeJS.ProcessEnv, home?: string): string;
15
+ /** Resolve the DSH home directory from the live environment. */
16
+ export declare function dshHome(): string;
@@ -38,7 +38,7 @@ export interface Config {
38
38
  }
39
39
  export declare const Config: z<Config>;
40
40
  /** Model-facing announcement: plugin presence, capabilities, and limits. */
41
- export declare const SSH_GUIDANCE = "\u672C\u673A\u5DF2\u5B89\u88C5 dsh-ssh \u63D2\u4EF6\uFF08DSH \u8FDC\u7A0B SSH \u8FD0\u7EF4\uFF09\uFF1A\u4FA7\u8FB9\u680F\u300CSSH\u300D\u5165\u53E3\uFF1B\u5728 dsh-web-ui \u63D2\u4EF6\u5168\u5BB6\u6876\u4ED3\u5E93\uFF08packages/dsh-ssh\uFF09\u7EDF\u4E00\u7EF4\u62A4\u3002\u80FD\u529B\uFF1A\u4E3B\u673A\u914D\u7F6E\u5B58 ~/.dsh/dsh-ssh.json\uFF08\u53EF\u4ECE ~/.ssh/config \u5BFC\u5165\uFF09\uFF1B\u6301\u4E45\u8FDE\u63A5\u6C60\u590D\u7528\u957F\u8FDE\u63A5\uFF08\u7A7A\u95F2 30 \u5206\u949F\u81EA\u52A8\u65AD\u5F00\uFF09\uFF1Bssh_list \u5217\u51FA\u4E3B\u673A\u3001ssh_exec \u6267\u884C\u8FDC\u7A0B\u547D\u4EE4\u3001ssh_upload/ssh_download \u4F20\u8F93\u6587\u4EF6\u3001ssh_tunnel \u672C\u5730\u7AEF\u53E3\u8F6C\u53D1\uFF08\u8BBF\u95EE\u8FDC\u7A0B\u6570\u636E\u5E93/\u5185\u7F51\u670D\u52A1\uFF09\u3001ssh_cluster \u96C6\u7FA4\u5E76\u53D1\u6267\u884C\uFF1B\u652F\u6301\u5BC6\u94A5/\u5BC6\u7801/ssh-agent \u8BA4\u8BC1\u3001passphrase \u5BC6\u94A5\u4E0E ProxyJump \u8DF3\u677F\u673A\uFF1BWeb \u7EC8\u7AEF\u8D70 WebSocket\u3002\u9650\u5236\uFF1A\u4E3B\u673A\u64CD\u4F5C\u7531\u7528\u6237\u5728 GUI \u4E2D\u914D\u7F6E\u540E agent \u65B9\u53EF\u4F7F\u7528\uFF1B\u5BC6\u7801\u4EE5\u660E\u6587\u5B58\u5728\u7528\u6237\u4E3B\u76EE\u5F55\u79C1\u6709\u6587\u4EF6\uFF08\u6743\u9650 0600\uFF09\uFF1B\u547D\u4EE4\u8F93\u51FA\u539F\u6837\u8FD4\u56DE\u3001\u53EF\u80FD\u542B\u654F\u611F\u4FE1\u606F\uFF1B\u65AD\u7EBF\u91CD\u8FDE\u53EF\u80FD\u91CD\u653E\u975E\u5E42\u7B49\u547D\u4EE4\uFF1B\u4F20\u8F93/\u6267\u884C\u6D88\u8017\u771F\u5B9E\u8FDC\u7A0B\u8D44\u6E90\uFF0C\u5148\u786E\u8BA4\u518D\u64CD\u4F5C\u3002\u7528\u6237\u63D0\u5230\u300CSSH / \u8FDC\u7A0B\u670D\u52A1\u5668 / \u670D\u52A1\u5668\u64CD\u4F5C / \u8DF3\u677F\u673A / \u96A7\u9053 / \u90E8\u7F72 / \u4E0A\u4F20\u4E0B\u8F7D\u300D\u65F6\u5373\u6307\u672C\u63D2\u4EF6\uFF0C\u8BF7\u636E\u6B64\u534F\u4F5C\u3002";
41
+ export declare const SSH_GUIDANCE = "\u672C\u673A\u5DF2\u5B89\u88C5 dsh-ssh \u63D2\u4EF6\uFF08DSH \u8FDC\u7A0B SSH \u8FD0\u7EF4\uFF09\uFF1A\u4FA7\u8FB9\u680F\u300CSSH\u300D\u5165\u53E3\uFF1B\u5728 dsh-web-ui \u63D2\u4EF6\u5168\u5BB6\u6876\u4ED3\u5E93\uFF08packages/dsh-ssh\uFF09\u7EDF\u4E00\u7EF4\u62A4\u3002\u80FD\u529B\uFF1A\u4E3B\u673A\u914D\u7F6E\u5B58 $DSH_HOME/dsh-ssh.json\uFF08\u9ED8\u8BA4 ~/.dsh\uFF09\uFF08\u53EF\u4ECE ~/.ssh/config \u5BFC\u5165\uFF09\uFF1B\u6301\u4E45\u8FDE\u63A5\u6C60\u590D\u7528\u957F\u8FDE\u63A5\uFF08\u7A7A\u95F2 30 \u5206\u949F\u81EA\u52A8\u65AD\u5F00\uFF09\uFF1Bssh_list \u5217\u51FA\u4E3B\u673A\u3001ssh_exec \u6267\u884C\u8FDC\u7A0B\u547D\u4EE4\u3001ssh_upload/ssh_download \u4F20\u8F93\u6587\u4EF6\u3001ssh_tunnel \u672C\u5730\u7AEF\u53E3\u8F6C\u53D1\uFF08\u8BBF\u95EE\u8FDC\u7A0B\u6570\u636E\u5E93/\u5185\u7F51\u670D\u52A1\uFF09\u3001ssh_cluster \u96C6\u7FA4\u5E76\u53D1\u6267\u884C\uFF1B\u652F\u6301\u5BC6\u94A5/\u5BC6\u7801/ssh-agent \u8BA4\u8BC1\u3001passphrase \u5BC6\u94A5\u4E0E ProxyJump \u8DF3\u677F\u673A\uFF1BWeb \u7EC8\u7AEF\u8D70 WebSocket\u3002\u9650\u5236\uFF1A\u4E3B\u673A\u64CD\u4F5C\u7531\u7528\u6237\u5728 GUI \u4E2D\u914D\u7F6E\u540E agent \u65B9\u53EF\u4F7F\u7528\uFF1B\u5BC6\u7801\u4EE5\u660E\u6587\u5B58\u5728\u7528\u6237\u4E3B\u76EE\u5F55\u79C1\u6709\u6587\u4EF6\uFF08\u6743\u9650 0600\uFF09\uFF1B\u547D\u4EE4\u8F93\u51FA\u539F\u6837\u8FD4\u56DE\u3001\u53EF\u80FD\u542B\u654F\u611F\u4FE1\u606F\uFF1B\u65AD\u7EBF\u91CD\u8FDE\u53EF\u80FD\u91CD\u653E\u975E\u5E42\u7B49\u547D\u4EE4\uFF1B\u4F20\u8F93/\u6267\u884C\u6D88\u8017\u771F\u5B9E\u8FDC\u7A0B\u8D44\u6E90\uFF0C\u5148\u786E\u8BA4\u518D\u64CD\u4F5C\u3002\u7528\u6237\u63D0\u5230\u300CSSH / \u8FDC\u7A0B\u670D\u52A1\u5668 / \u670D\u52A1\u5668\u64CD\u4F5C / \u8DF3\u677F\u673A / \u96A7\u9053 / \u90E8\u7F72 / \u4E0A\u4F20\u4E0B\u8F7D\u300D\u65F6\u5373\u6307\u672C\u63D2\u4EF6\uFF0C\u8BF7\u636E\u6B64\u534F\u4F5C\u3002";
42
42
  /**
43
43
  * Mount the SSH engine, routes, tools, and announcement.
44
44
  * @param ctx - host plugin context carrying webServer/tools/systemPrompt.
@@ -5,7 +5,7 @@
5
5
  */
6
6
  /** Authentication flavors a host entry may carry. */
7
7
  export type SshAuthKind = 'key' | 'password' | 'agent';
8
- /** One stored host entry (the ~/.dsh/dsh-ssh.json store shape). */
8
+ /** One stored host entry (the $DSH_HOME/dsh-ssh.json store shape). */
9
9
  export interface SshHostEntry {
10
10
  /** Stable, user-chosen identifier used by every operation. */
11
11
  alias: string;
@@ -1,12 +1,13 @@
1
1
  /**
2
- * Host config store: one JSON file (`~/.dsh/dsh-ssh.json`) holding every
2
+ * Host config store: one JSON file (`$DSH_HOME/dsh-ssh.json`, defaulting
3
+ * to `~/.dsh`) holding every
3
4
  * SSH host entry, written atomically (tmp + rename). Also parses the user's
4
5
  * standard `~/.ssh/config` for one-shot import. Secrets (passwords,
5
6
  * passphrases) live in this user-owned file in plaintext — same trust model
6
7
  * as ssh-skill's annotated ssh-config comments; document it, never log it.
7
8
  */
8
9
  import type { HostPayload, ImportResult, SshHostEntry, SshHostSummary } from './protocol.ts';
9
- /** Store file location: <home>/.dsh/dsh-ssh.json. */
10
+ /** Store file location: $DSH_HOME/dsh-ssh.json (defaults to ~/.dsh). */
10
11
  export declare function storePath(): string;
11
12
  /** The user's standard OpenSSH config path. */
12
13
  export declare function sshConfigPath(): string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@linxin666/dsh-ssh",
3
3
  "description": "Remote SSH operations for the dsh web GUI: host config store (~/.dsh/dsh-ssh.json, import from ~/.ssh/config), a persistent ssh2 connection pool with jump-host support, exec / PTY web terminal / SFTP transfer / local port-forward tunnels / cluster execution, plus agent tools (ssh_list, ssh_exec, ssh_upload, ssh_download, ssh_tunnel, ssh_cluster). Hot-pluggable — mounted via ~/.dsh/cordis.patch.yml + a profile node_modules symlink, no dsh source changes.",
4
- "version": "0.2.7",
4
+ "version": "0.2.8",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": "^22.19.0 || >=24.0.0"
@@ -25,6 +25,9 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dsh": {
28
+ "engines": {
29
+ "dsh": ">=0.1.1-rc.1"
30
+ },
28
31
  "bundle": {
29
32
  "patch": "./cordis.patch.yml"
30
33
  },
@@ -49,18 +52,18 @@
49
52
  },
50
53
  "devDependencies": {
51
54
  "@deepseek-ai/cordis": "^4.0.1",
52
- "@deepseek-ai/dsh-client-connection": "^0.1.1-rc.1",
53
- "@deepseek-ai/dsh-client-locale": "^0.1.1-rc.1",
54
- "@deepseek-ai/dsh-client-runtime": "^0.1.1-rc.1",
55
- "@deepseek-ai/dsh-client-ui-settings": "^0.1.1-rc.1",
56
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.1-rc.1",
57
- "@deepseek-ai/dsh-host-webserver": "^0.1.1-rc.1",
58
- "@deepseek-ai/dsh-llm": "^0.1.1-rc.1",
59
- "@deepseek-ai/dsh-scope": "^0.1.1-rc.1",
60
- "@deepseek-ai/dsh-settings": "^0.1.1-rc.1",
61
- "@deepseek-ai/dsh-system-prompt": "^0.1.1-rc.1",
62
- "@deepseek-ai/dsh-timeout": "^0.1.1-rc.1",
63
- "@deepseek-ai/dsh-tools": "^0.1.1-rc.1",
55
+ "@deepseek-ai/dsh-client-connection": "^0.1.1-rc.2",
56
+ "@deepseek-ai/dsh-client-locale": "^0.1.1-rc.2",
57
+ "@deepseek-ai/dsh-client-runtime": "^0.1.1-rc.2",
58
+ "@deepseek-ai/dsh-client-ui-settings": "^0.1.1-rc.2",
59
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.1-rc.2",
60
+ "@deepseek-ai/dsh-host-webserver": "^0.1.1-rc.2",
61
+ "@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
62
+ "@deepseek-ai/dsh-scope": "^0.1.1-rc.2",
63
+ "@deepseek-ai/dsh-settings": "^0.1.1-rc.2",
64
+ "@deepseek-ai/dsh-system-prompt": "^0.1.1-rc.2",
65
+ "@deepseek-ai/dsh-timeout": "^0.1.1-rc.2",
66
+ "@deepseek-ai/dsh-tools": "^0.1.1-rc.2",
64
67
  "@types/node": "^22.20.0",
65
68
  "@types/react": "~18.3.1",
66
69
  "@types/react-dom": "^18.3.5",
@@ -62,7 +62,7 @@ export const zh = {
62
62
  'form.keyPathHint': '如 ~/.ssh/id_ed25519',
63
63
  'form.passphrase': '密钥口令',
64
64
  'form.password': '密码',
65
- 'form.passwordHint': '明文保存在 ~/.dsh/dsh-ssh.json(本机私有文件)',
65
+ 'form.passwordHint': '明文保存在 $DSH_HOME/dsh-ssh.json(本机私有文件)',
66
66
  'form.passwordRequired': '密码认证需要填写密码。',
67
67
  'form.proxyJump': '跳板机别名',
68
68
  'form.proxyJumpHint': '逗号分隔的本地主机别名,按顺序连接',
@@ -209,7 +209,7 @@ export const en: Record<keyof typeof zh, string> = {
209
209
  'form.keyPathHint': 'e.g. ~/.ssh/id_ed25519',
210
210
  'form.passphrase': 'Key passphrase',
211
211
  'form.password': 'Password',
212
- 'form.passwordHint': 'Stored in plaintext in ~/.dsh/dsh-ssh.json (private local file)',
212
+ 'form.passwordHint': 'Stored in plaintext in $DSH_HOME/dsh-ssh.json (private local file)',
213
213
  'form.passwordRequired': 'Password auth requires a password.',
214
214
  'form.proxyJump': 'Jump host aliases',
215
215
  'form.proxyJumpHint': 'Comma-separated local aliases, chained in order',
@@ -105,15 +105,21 @@ export function HostsTab({ api, onConnect }: HostsTabProps) {
105
105
  return () => { clearTimeout(timer) }
106
106
  }, [search, load])
107
107
 
108
+ // Every async setState path guards with mountedRef, not just load(): a
109
+ // promise settling after unmount would setState against the torn-down
110
+ // environment (window is not defined, main-CI flake).
108
111
  const runTest = async (alias: string): Promise<void> => {
112
+ if (!mountedRef.current) return
109
113
  setTestingAlias(alias)
110
114
  try {
111
115
  const result = await api.testHost(alias)
116
+ if (!mountedRef.current) return
112
117
  setTestResults(prev => ({ ...prev, [alias]: result }))
113
118
  } catch (cause) {
119
+ if (!mountedRef.current) return
114
120
  setTestResults(prev => ({ ...prev, [alias]: { ok: false, error: errorMessage(cause) } }))
115
121
  } finally {
116
- setTestingAlias(null)
122
+ if (mountedRef.current) setTestingAlias(null)
117
123
  }
118
124
  }
119
125
 
@@ -121,32 +127,38 @@ export function HostsTab({ api, onConnect }: HostsTabProps) {
121
127
  if (!window.confirm(tt('hosts.deleteConfirm', { alias }))) return
122
128
  try {
123
129
  await api.deleteHost(alias)
130
+ if (!mountedRef.current) return
124
131
  void load()
125
132
  } catch (cause) {
133
+ if (!mountedRef.current) return
126
134
  setError(errorMessage(cause))
127
135
  }
128
136
  }
129
137
 
130
138
  // Group-header batch action (#379): test every host in the group.
131
139
  const testGroup = async (group: HostGroup): Promise<void> => {
140
+ if (!mountedRef.current) return
132
141
  setTestingGroup(group.key)
133
142
  try {
134
143
  await Promise.all(group.hosts.map(host => runTest(host.alias)))
135
144
  } finally {
136
- setTestingGroup(null)
145
+ if (mountedRef.current) setTestingGroup(null)
137
146
  }
138
147
  }
139
148
 
140
149
  const importConfig = async (): Promise<void> => {
150
+ if (!mountedRef.current) return
141
151
  setImporting(true)
142
152
  try {
143
153
  const result = await api.importSshConfig()
154
+ if (!mountedRef.current) return
144
155
  setNotice(tt('hosts.imported', { parsed: result.parsed, added: result.added, skipped: result.skipped }))
145
156
  void load()
146
157
  } catch (cause) {
158
+ if (!mountedRef.current) return
147
159
  setError(errorMessage(cause))
148
160
  } finally {
149
- setImporting(false)
161
+ if (mountedRef.current) setImporting(false)
150
162
  }
151
163
  }
152
164
 
@@ -49,12 +49,13 @@ html[data-dsh-ssh-active]:not([data-dsh-taskboard-active]) [class*='centerCol']
49
49
  /* --- sidebar entry row ------------------------------------------------------- */
50
50
 
51
51
  .entry {
52
+ box-sizing: border-box;
52
53
  display: flex;
53
54
  align-items: center;
54
- gap: 8px;
55
+ gap: 10px;
55
56
  width: 100%;
56
- height: 32px;
57
- padding: 0 12px;
57
+ min-height: 36px;
58
+ padding: 0 10px;
58
59
  background: transparent;
59
60
  border: none;
60
61
  border-radius: 8px;
@@ -79,9 +80,17 @@ html[data-dsh-ssh-active]:not([data-dsh-taskboard-active]) [class*='centerCol']
79
80
  display: inline-flex;
80
81
  align-items: center;
81
82
  justify-content: center;
83
+ width: 24px;
84
+ height: 24px;
82
85
  flex: none;
83
86
  }
84
87
 
88
+ .entryIcon svg {
89
+ display: block;
90
+ width: 18px;
91
+ height: 18px;
92
+ }
93
+
85
94
  .entryLabel {
86
95
  overflow: hidden;
87
96
  text-overflow: ellipsis;
@@ -91,7 +100,9 @@ html[data-dsh-ssh-active]:not([data-dsh-taskboard-active]) [class*='centerCol']
91
100
  [data-dsh-frame][data-sidebar-collapsed] .entry {
92
101
  justify-content: center;
93
102
  padding: 0;
94
- width: 100%;
103
+ width: 40px;
104
+ min-height: 40px;
105
+ margin-inline: auto;
95
106
  }
96
107
 
97
108
  [data-dsh-frame][data-sidebar-collapsed] .entryLabel {
@@ -16,8 +16,8 @@ import { mountSidebarEntry as mountSharedSidebarEntry } from './sidebar-entry-co
16
16
  /** Stable data attribute identifying the injected entry row. */
17
17
  export const ENTRY_SELECTOR = '[data-dsh-ssh-entry]'
18
18
 
19
- /** Inline icon (matches the shell's 16px nav-icon look): a terminal prompt glyph. */
20
- const ICON = '<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="2.5" width="12" height="11" rx="1.5"/><path d="M4.5 5.5l2.5 2.5-2.5 2.5"/><path d="M8.5 10.5h3"/></svg>'
19
+ /** Inline terminal glyph sized to the shell's current sidebar navigation icons. */
20
+ const ICON = '<svg viewBox="0 0 16 16" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="1.75" y="2.25" width="12.5" height="11.5" rx="1.75"/><path d="M4.25 5.25l2.75 2.75-2.75 2.75"/><path d="M8.5 10.75h3.25"/></svg>'
21
21
 
22
22
  /**
23
23
  * Mount the sidebar entry, waiting for the shell to render and self-healing
@@ -0,0 +1,36 @@
1
+ // Generated by scripts/sync-shared.mjs from shared/host/dsh-home.ts. Do not edit this copy; edit the shared source and run "node scripts/sync-shared.mjs".
2
+ /**
3
+ * DSH_HOME resolution shared by the plugin family's Host halves: the
4
+ * environment override wins, the platform home fallback follows. Mirrors
5
+ * what dsh-pet and dsh-liangshen each used to implement locally.
6
+ */
7
+
8
+ import { homedir } from 'node:os'
9
+ import { isAbsolute, join } from 'node:path'
10
+
11
+ /** Expand a leading ~ (or ~user) in a path, platform-style. */
12
+ export function expandHome(path: string, home: string = homedir()): string {
13
+ if (path === '~') return home
14
+ if (path.startsWith('~/') || path.startsWith('~\\')) return join(home, path.slice(2))
15
+ return path
16
+ }
17
+
18
+ /**
19
+ * Resolve the DSH home directory.
20
+ * @param env - process environment to read DSH_HOME from.
21
+ * @param home - platform home directory fallback (test seam).
22
+ * @returns the absolute DSH home path.
23
+ */
24
+ export function resolveDshHome(env: NodeJS.ProcessEnv = process.env, home: string = homedir()): string {
25
+ const raw = env.DSH_HOME
26
+ if (raw !== undefined && raw.trim() !== '') {
27
+ const expanded = expandHome(raw.trim(), home)
28
+ return isAbsolute(expanded) ? expanded : join(process.cwd(), expanded)
29
+ }
30
+ return join(home, '.dsh')
31
+ }
32
+
33
+ /** Resolve the DSH home directory from the live environment. */
34
+ export function dshHome(): string {
35
+ return resolveDshHome()
36
+ }
package/src/index.ts CHANGED
@@ -64,7 +64,7 @@ const DEFAULT_ANNOUNCE = false
64
64
  const SECTION_ORDER = 150
65
65
 
66
66
  /** Model-facing announcement: plugin presence, capabilities, and limits. */
67
- export const SSH_GUIDANCE = '本机已安装 dsh-ssh 插件(DSH 远程 SSH 运维):侧边栏「SSH」入口;在 dsh-web-ui 插件全家桶仓库(packages/dsh-ssh)统一维护。能力:主机配置存 ~/.dsh/dsh-ssh.json(可从 ~/.ssh/config 导入);持久连接池复用长连接(空闲 30 分钟自动断开);ssh_list 列出主机、ssh_exec 执行远程命令、ssh_upload/ssh_download 传输文件、ssh_tunnel 本地端口转发(访问远程数据库/内网服务)、ssh_cluster 集群并发执行;支持密钥/密码/ssh-agent 认证、passphrase 密钥与 ProxyJump 跳板机;Web 终端走 WebSocket。限制:主机操作由用户在 GUI 中配置后 agent 方可使用;密码以明文存在用户主目录私有文件(权限 0600);命令输出原样返回、可能含敏感信息;断线重连可能重放非幂等命令;传输/执行消耗真实远程资源,先确认再操作。用户提到「SSH / 远程服务器 / 服务器操作 / 跳板机 / 隧道 / 部署 / 上传下载」时即指本插件,请据此协作。'
67
+ export const SSH_GUIDANCE = '本机已安装 dsh-ssh 插件(DSH 远程 SSH 运维):侧边栏「SSH」入口;在 dsh-web-ui 插件全家桶仓库(packages/dsh-ssh)统一维护。能力:主机配置存 $DSH_HOME/dsh-ssh.json(默认 ~/.dsh)(可从 ~/.ssh/config 导入);持久连接池复用长连接(空闲 30 分钟自动断开);ssh_list 列出主机、ssh_exec 执行远程命令、ssh_upload/ssh_download 传输文件、ssh_tunnel 本地端口转发(访问远程数据库/内网服务)、ssh_cluster 集群并发执行;支持密钥/密码/ssh-agent 认证、passphrase 密钥与 ProxyJump 跳板机;Web 终端走 WebSocket。限制:主机操作由用户在 GUI 中配置后 agent 方可使用;密码以明文存在用户主目录私有文件(权限 0600);命令输出原样返回、可能含敏感信息;断线重连可能重放非幂等命令;传输/执行消耗真实远程资源,先确认再操作。用户提到「SSH / 远程服务器 / 服务器操作 / 跳板机 / 隧道 / 部署 / 上传下载」时即指本插件,请据此协作。'
68
68
 
69
69
  /**
70
70
  * Mount the SSH engine, routes, tools, and announcement.
package/src/protocol.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  /** Authentication flavors a host entry may carry. */
8
8
  export type SshAuthKind = 'key' | 'password' | 'agent'
9
9
 
10
- /** One stored host entry (the ~/.dsh/dsh-ssh.json store shape). */
10
+ /** One stored host entry (the $DSH_HOME/dsh-ssh.json store shape). */
11
11
  export interface SshHostEntry {
12
12
  /** Stable, user-chosen identifier used by every operation. */
13
13
  alias: string
package/src/store.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  /**
2
- * Host config store: one JSON file (`~/.dsh/dsh-ssh.json`) holding every
2
+ * Host config store: one JSON file (`$DSH_HOME/dsh-ssh.json`, defaulting
3
+ * to `~/.dsh`) holding every
3
4
  * SSH host entry, written atomically (tmp + rename). Also parses the user's
4
5
  * standard `~/.ssh/config` for one-shot import. Secrets (passwords,
5
6
  * passphrases) live in this user-owned file in plaintext — same trust model
@@ -9,14 +10,15 @@
9
10
  import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs'
10
11
  import { homedir } from 'node:os'
11
12
  import { dirname, join, resolve } from 'node:path'
13
+ import { dshHome } from './dsh-home.ts'
12
14
  import type { HostPayload, ImportResult, SshHostEntry, SshHostSummary } from './protocol.ts'
13
15
 
14
16
  /** File format version. */
15
17
  const FORMAT_VERSION = 1
16
18
 
17
- /** Store file location: <home>/.dsh/dsh-ssh.json. */
19
+ /** Store file location: $DSH_HOME/dsh-ssh.json (defaults to ~/.dsh). */
18
20
  export function storePath(): string {
19
- return join(homedir(), '.dsh', 'dsh-ssh.json')
21
+ return join(dshHome(), 'dsh-ssh.json')
20
22
  }
21
23
 
22
24
  /** The user's standard OpenSSH config path. */