@hyzyn/dsh-tty 0.12.0 → 0.15.0

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
@@ -16,7 +16,7 @@ import { definePlugin } from '@hyzyn/dsh-kit';
16
16
  import { defineTool } from '@deepseek-ai/dsh-tools';
17
17
  import { spawnSsh, sshTarget, expandHome } from './ssh.js';
18
18
  import { probeSsh } from './probe.js';
19
- import { buildShellSpawn } from './shell-integration.js';
19
+ import { buildCommandSpawn, buildShellSpawn } from './shell-integration.js';
20
20
  import { parseSshConfig } from './ssh-config.js';
21
21
  import { parseKnownHosts } from './known-hosts.js';
22
22
  import { TunnelManager } from './tunnels.js';
@@ -88,6 +88,28 @@ const DEFAULT_RECONNECT_GRACE_SEC = 120;
88
88
  const BACKPRESSURE_HIGH = 512 * 1024;
89
89
  const BACKPRESSURE_LOW = 128 * 1024;
90
90
  const SID_RE = /^[A-Za-z0-9_-]{1,64}$/;
91
+ /** 自定义命令标签(0.14.0)的长度上限:单条命令,防误传超长脚本。 */
92
+ const COMMAND_MAX = 2000;
93
+ /**
94
+ * 清洗帧里的 `command`(0.14.0,ttyTerminal 服务用):必须是单行、非空、
95
+ * 长度受控的字符串。命令来自**宿主侧插件**(如 dsh-docker 的
96
+ * `docker exec -it <容器> sh`),信任级与插件本身相同;这里只做形状校验,
97
+ * 避免换行破坏本地 `-c` 包装层、或超长内容拖垮帧解析。
98
+ */
99
+ function sanitizeCommand(value) {
100
+ if (value === undefined)
101
+ return {};
102
+ if (typeof value !== 'string')
103
+ return { error: 'command 必须是字符串' };
104
+ const trimmed = value.trim();
105
+ if (trimmed === '')
106
+ return { error: 'command 不能为空' };
107
+ if (trimmed.length > COMMAND_MAX)
108
+ return { error: `command 过长(≤${String(COMMAND_MAX)} 字符)` };
109
+ if (/[\r\n\0]/.test(trimmed))
110
+ return { error: 'command 必须是单行(不能含换行/NUL)' };
111
+ return { command: trimmed };
112
+ }
91
113
  const BUFFER_CAP = 256 * 1024;
92
114
  /** TERM/COLORTERM 白名单:防止值里的引号破坏 -c 包装层命令(shellArgv 单引号包裹)。 */
93
115
  const TERM_RE = /^[A-Za-z0-9_.+-]+$/;
@@ -930,7 +952,14 @@ class TtyServer {
930
952
  // 换成 `exec tmux -L dsh-tty -A -s <名>`(tmux 托管);tmux 未安装则降级
931
953
  // 普通会话并回灰字提示。持久名稳定(客户端生成、随标签规格保存),
932
954
  // 宿主重启后重开标签按同名 attach 回原 tmux 会话
933
- const persistName = msg.persist === true && this.options.persistence === 'tmux'
955
+ // 命令标签(0.14.0):直接跑一条命令,不做 tmux 持久化(命令短命)
956
+ const parsedCommand = sanitizeCommand(msg.command);
957
+ if (parsedCommand.error !== undefined) {
958
+ send(ws, { t: 'error', sid, m: parsedCommand.error });
959
+ return;
960
+ }
961
+ const command = parsedCommand.command ?? null;
962
+ const persistName = command === null && msg.persist === true && this.options.persistence === 'tmux'
934
963
  ? sanitizePersistName(msg.persistName, sid)
935
964
  : null;
936
965
  // 跨窗口共享(0.10.1):同 tmuxName 的宿主会话还活着(别的窗口接回过,
@@ -959,7 +988,9 @@ class TtyServer {
959
988
  return;
960
989
  }
961
990
  const wantsPersist = persistName !== null;
962
- let spawnPlan = buildShellSpawn(this.options.shell, this.options.term, this.options.colorTerm, this.options.shellIntegration);
991
+ let spawnPlan = command !== null
992
+ ? buildCommandSpawn(this.options.shell, this.options.term, this.options.colorTerm, command)
993
+ : buildShellSpawn(this.options.shell, this.options.term, this.options.colorTerm, this.options.shellIntegration);
963
994
  let tmuxName = null;
964
995
  if (wantsPersist) {
965
996
  const probe = await probeTmux();
@@ -1037,7 +1068,13 @@ class TtyServer {
1037
1068
  }
1038
1069
  // 跨窗口共享(0.10.1):同 tmuxName 的 SSH 持久会话还活着时不重建远程
1039
1070
  // 连接,本连接重绑定到现有会话(单 PTY 多客户端扇出)
1040
- const persistName = msg.persist === true && this.options.persistence === 'tmux'
1071
+ const parsedCommand = sanitizeCommand(msg.command);
1072
+ if (parsedCommand.error !== undefined) {
1073
+ send(ws, { t: 'error', sid, m: parsedCommand.error });
1074
+ return;
1075
+ }
1076
+ const command = parsedCommand.command ?? null;
1077
+ const persistName = command === null && msg.persist === true && this.options.persistence === 'tmux'
1041
1078
  ? sanitizePersistName(msg.persistName, sid)
1042
1079
  : null;
1043
1080
  if (persistName !== null) {
@@ -1074,6 +1111,7 @@ class TtyServer {
1074
1111
  rows: Number(msg.rows) || 24,
1075
1112
  logger: { info: (m) => this.ctx.logger.info(m), warn: (m) => this.ctx.logger.warn(m) },
1076
1113
  hostKeyStore: this.hostKeyStore,
1114
+ ...(command !== null ? { command } : {}),
1077
1115
  ...(persistOpt !== undefined ? { persist: persistOpt } : {}),
1078
1116
  });
1079
1117
  }