@linxin666/dsh-ssh 0.1.20 → 0.2.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
@@ -2553,13 +2553,22 @@ function makeRoutes(deps) {
2553
2553
  ok: false,
2554
2554
  error: error instanceof Error ? error.message : String(error)
2555
2555
  });
2556
- try {
2557
- sink.destroy();
2558
- } catch {}
2559
- unlink(tmp).catch(() => void 0);
2560
- try {
2561
- res.end();
2562
- } catch {}
2556
+ const cleanup = () => {
2557
+ unlink(tmp).catch(() => void 0).finally(() => {
2558
+ try {
2559
+ res.end();
2560
+ } catch {}
2561
+ });
2562
+ };
2563
+ if (sink.destroyed) cleanup();
2564
+ else {
2565
+ sink.once("close", cleanup);
2566
+ try {
2567
+ sink.destroy();
2568
+ } catch {
2569
+ cleanup();
2570
+ }
2571
+ }
2563
2572
  };
2564
2573
  const done = () => {
2565
2574
  if (settled) return;
@@ -2576,13 +2585,18 @@ function makeRoutes(deps) {
2576
2585
  if (!res.writableEnded) fail("connection closed");
2577
2586
  });
2578
2587
  let received = 0;
2588
+ let capped = false;
2579
2589
  req.on("data", (chunk) => {
2580
2590
  received += chunk.byteLength;
2581
- if (received > maxUploadBytes) {
2591
+ if (received > maxUploadBytes && !capped) {
2592
+ capped = true;
2582
2593
  fail("upload body too large");
2583
- try {
2584
- req.destroy();
2585
- } catch {}
2594
+ res.on("finish", () => {
2595
+ try {
2596
+ req.destroy();
2597
+ } catch {}
2598
+ });
2599
+ req.resume();
2586
2600
  }
2587
2601
  });
2588
2602
  req.pipe(sink);
@@ -2,133 +2,141 @@
2
2
  * dsh-ssh surface copy: zh is the key source, en mirrors every key.
3
3
  */
4
4
  export declare const zh: {
5
- readonly 'entry.label': 'SSH';
6
- readonly 'entry.tooltip': '远程 SSH 运维面板';
7
- readonly 'panel.title': '远程 SSH';
8
- readonly 'panel.backToConversation': '返回会话';
9
- readonly 'tab.hosts': '主机';
10
- readonly 'tab.terminal': '终端';
11
- readonly 'tab.transfer': '传输';
12
- readonly 'tab.tunnels': '隧道';
13
- readonly 'tab.cluster': '集群';
14
- readonly 'hosts.empty': '尚未配置主机。点击「新增主机」添加,或从 ~/.ssh/config 导入。';
15
- readonly 'hosts.add': '新增主机';
16
- readonly 'hosts.import': '导入 ~/.ssh/config';
17
- readonly 'hosts.search': '搜索别名 / 描述 / 标签…';
18
- readonly 'hosts.col.alias': '别名';
19
- readonly 'hosts.col.host': '主机';
20
- readonly 'hosts.col.user': '用户';
21
- readonly 'hosts.col.auth': '认证';
22
- readonly 'hosts.col.environment': '环境';
23
- readonly 'hosts.col.tags': '标签';
24
- readonly 'hosts.col.description': '备注';
25
- readonly 'hosts.col.actions': '操作';
26
- readonly 'hosts.test': '测试';
27
- readonly 'hosts.edit': '编辑';
28
- readonly 'hosts.delete': '删除';
29
- readonly 'hosts.deleteConfirm': '确定删除主机「{alias}」?将同时停止其隧道。';
30
- readonly 'hosts.imported': '解析 {parsed} 个配置块,新增 {added} 台主机,跳过 {skipped} 个。';
31
- readonly 'hosts.testing': '连接测试中…';
32
- readonly 'hosts.testOk': '连接成功({latency} ms)';
33
- readonly 'hosts.testFail': '连接失败:{error}';
34
- readonly 'hosts.connected': '连接';
35
- readonly 'hosts.notConnected': '未连接';
36
- readonly 'form.title.create': '新增主机';
37
- readonly 'form.title.edit': '编辑主机 {alias}';
38
- readonly 'form.alias': '别名';
39
- readonly 'form.aliasHint': '小写字母、数字与单连字符,操作时使用';
40
- readonly 'form.host': '主机地址';
41
- readonly 'form.port': '端口';
42
- readonly 'form.user': '用户名';
43
- readonly 'form.auth': '认证方式';
44
- readonly 'form.auth.key': '密钥';
45
- readonly 'form.auth.password': '密码';
46
- readonly 'form.authKeepHint': '编辑时密钥/密码留空将保留原认证方式与已存凭据;如需更换认证方式请填写新凭据。';
47
- readonly 'form.keyPath': '私钥路径';
48
- readonly 'form.keyPathHint': '如 ~/.ssh/id_ed25519';
49
- readonly 'form.passphrase': '密钥口令';
50
- readonly 'form.password': '密码';
51
- readonly 'form.passwordHint': '明文保存在 ~/.dsh/dsh-ssh.json(本机私有文件)';
52
- readonly 'form.proxyJump': '跳板机别名';
53
- readonly 'form.proxyJumpHint': '逗号分隔的本地主机别名,按顺序连接';
54
- readonly 'form.description': '备注';
55
- readonly 'form.environment': '环境';
56
- readonly 'form.environmentHint': '如 development / production';
57
- readonly 'form.tags': '标签';
58
- readonly 'form.tagsHint': '逗号分隔';
59
- readonly 'form.location': '位置';
60
- readonly 'form.save': '保存';
61
- readonly 'form.cancel': '取消';
62
- readonly 'form.required': '请填写必填字段:别名、主机地址、用户名。';
63
- readonly 'form.portInvalid': '端口必须是 1-65535 之间的整数。';
64
- readonly 'terminal.selectHost': '选择主机';
65
- readonly 'terminal.connect': '连接';
66
- readonly 'terminal.disconnect': '断开';
67
- readonly 'terminal.placeholder': '选择主机后点击「连接」打开远程终端。';
68
- readonly 'terminal.connecting': '正在连接…';
69
- readonly 'terminal.ready': '终端已连接({alias})';
70
- readonly 'terminal.exited': '终端已退出({alias})';
71
- readonly 'terminal.error': '终端错误:{error}';
72
- readonly 'transfer.selectHost': '选择主机';
73
- readonly 'transfer.remotePath': '远程路径';
74
- readonly 'transfer.remotePathHint': '如 /tmp/ 或 /home/user/app.tar.gz';
75
- readonly 'transfer.upload': '上传';
76
- readonly 'transfer.uploading': '正在上传 {file}…';
77
- readonly 'transfer.download': '下载';
78
- readonly 'transfer.downloading': '正在下载…';
79
- readonly 'transfer.done': '完成:{bytes} 字节';
80
- readonly 'transfer.failed': '传输失败:{error}';
81
- readonly 'transfer.browseRemote': '浏览远程目录';
82
- readonly 'transfer.refresh': '刷新';
83
- readonly 'transfer.name': '名称';
84
- readonly 'transfer.upLevel': '上级目录';
85
- readonly 'transfer.size': '大小';
86
- readonly 'transfer.modified': '修改时间';
87
- readonly 'transfer.bytes': '{value} B';
88
- readonly 'transfer.kib': '{value} KB';
89
- readonly 'transfer.mib': '{value} MB';
90
- readonly 'transfer.gib': '{value} GB';
91
- readonly 'transfer.percent': '{value}%';
92
- readonly 'transfer.speed': '{value}/s';
93
- readonly 'transfer.dir': '目录';
94
- readonly 'transfer.file': '文件';
95
- readonly 'tunnel.empty': '没有活跃的隧道。';
96
- readonly 'tunnel.start': '新建隧道';
97
- readonly 'tunnel.alias': '主机别名';
98
- readonly 'tunnel.remotePort': '远程端口';
99
- readonly 'tunnel.remoteHost': '远程主机';
100
- readonly 'tunnel.remoteHostHint': '默认 127.0.0.1(服务器自身)';
101
- readonly 'tunnel.localPort': '本地端口';
102
- readonly 'tunnel.localPortHint': '留空自动分配';
103
- readonly 'tunnel.stop': '停止';
104
- readonly 'tunnel.stopAll': '全部停止';
105
- readonly 'tunnel.stopAllConfirm': '确定停止全部隧道吗?';
106
- readonly 'tunnel.row': '{alias}: 127.0.0.1:{localPort} -> {remoteHost}:{remotePort}';
107
- readonly 'tunnel.started': '隧道已建立:127.0.0.1:{localPort}';
108
- readonly 'tunnel.failed': '隧道启动失败:{error}';
109
- readonly 'cluster.command': '命令';
110
- readonly 'cluster.run': '执行';
111
- readonly 'cluster.confirm': '确定在匹配的主机上执行该命令吗?该操作会真实运行在远程服务器上。';
112
- readonly 'cluster.aliases': '主机别名(逗号分隔,留空为全部)';
113
- readonly 'cluster.environment': '环境过滤';
114
- readonly 'cluster.tags': '标签过滤(逗号分隔)';
115
- readonly 'cluster.results': '执行结果';
116
- readonly 'cluster.empty': '输入命令后点击「执行」。';
117
- readonly 'cluster.noMatch': '没有匹配的主机。';
118
- readonly 'cluster.ok': '成功';
119
- readonly 'cluster.fail': '失败';
120
- readonly 'cluster.timeout': '超时';
121
- readonly 'cluster.col.alias': '主机';
122
- readonly 'cluster.col.status': '状态';
123
- readonly 'cluster.col.exit': '退出码';
124
- readonly 'cluster.col.stdout': '标准输出';
125
- readonly 'cluster.col.stderr': '标准错误';
126
- readonly 'cluster.col.error': '错误';
127
- readonly 'cluster.col.duration': '耗时';
128
- readonly 'common.loading': '加载中…';
129
- readonly 'common.error': '出错:{error}';
130
- readonly 'common.refresh': '刷新';
131
- readonly 'common.confirm': '确定';
5
+ readonly 'entry.label': "SSH";
6
+ readonly 'entry.tooltip': "远程 SSH 运维面板";
7
+ readonly 'panel.title': "远程 SSH";
8
+ readonly 'panel.backToConversation': "返回会话";
9
+ readonly 'tab.hosts': "主机";
10
+ readonly 'tab.terminal': "终端";
11
+ readonly 'tab.transfer': "传输";
12
+ readonly 'tab.tunnels': "隧道";
13
+ readonly 'tab.cluster': "集群";
14
+ readonly 'hosts.empty': "尚未配置主机。点击「新增主机」添加,或从 ~/.ssh/config 导入。";
15
+ readonly 'hosts.add': "新增主机";
16
+ readonly 'hosts.import': "导入 ~/.ssh/config";
17
+ readonly 'hosts.search': "搜索别名 / 描述 / 标签…";
18
+ readonly 'hosts.groupBy.label': "分组方式";
19
+ readonly 'hosts.groupBy.none': "不分组";
20
+ readonly 'hosts.groupBy.environment': "按环境分组";
21
+ readonly 'hosts.groupBy.tags': "按标签分组";
22
+ readonly 'hosts.group.ungrouped': "未设置环境";
23
+ readonly 'hosts.group.noTags': "无标签";
24
+ readonly 'hosts.group.count': "{count} 台";
25
+ readonly 'hosts.group.testAll': "测试全部";
26
+ readonly 'hosts.col.alias': "别名";
27
+ readonly 'hosts.col.host': "主机";
28
+ readonly 'hosts.col.user': "用户";
29
+ readonly 'hosts.col.auth': "认证";
30
+ readonly 'hosts.col.environment': "环境";
31
+ readonly 'hosts.col.tags': "标签";
32
+ readonly 'hosts.col.description': "备注";
33
+ readonly 'hosts.col.actions': "操作";
34
+ readonly 'hosts.test': "测试";
35
+ readonly 'hosts.edit': "编辑";
36
+ readonly 'hosts.delete': "删除";
37
+ readonly 'hosts.deleteConfirm': "确定删除主机「{alias}」?将同时停止其隧道。";
38
+ readonly 'hosts.imported': "解析 {parsed} 个配置块,新增 {added} 台主机,跳过 {skipped} 个。";
39
+ readonly 'hosts.testing': "连接测试中…";
40
+ readonly 'hosts.testOk': "连接成功({latency} ms)";
41
+ readonly 'hosts.testFail': "连接失败:{error}";
42
+ readonly 'hosts.connected': "连接";
43
+ readonly 'hosts.notConnected': "未连接";
44
+ readonly 'form.title.create': "新增主机";
45
+ readonly 'form.title.edit': "编辑主机 {alias}";
46
+ readonly 'form.alias': "别名";
47
+ readonly 'form.aliasHint': "小写字母、数字与单连字符,操作时使用";
48
+ readonly 'form.host': "主机地址";
49
+ readonly 'form.port': "端口";
50
+ readonly 'form.user': "用户名";
51
+ readonly 'form.auth': "认证方式";
52
+ readonly 'form.auth.key': "密钥";
53
+ readonly 'form.auth.password': "密码";
54
+ readonly 'form.authKeepHint': "编辑时密钥/密码留空将保留原认证方式与已存凭据;如需更换认证方式请填写新凭据。";
55
+ readonly 'form.keyPath': "私钥路径";
56
+ readonly 'form.keyPathHint': "如 ~/.ssh/id_ed25519";
57
+ readonly 'form.passphrase': "密钥口令";
58
+ readonly 'form.password': "密码";
59
+ readonly 'form.passwordHint': "明文保存在 ~/.dsh/dsh-ssh.json(本机私有文件)";
60
+ readonly 'form.proxyJump': "跳板机别名";
61
+ readonly 'form.proxyJumpHint': "逗号分隔的本地主机别名,按顺序连接";
62
+ readonly 'form.description': "备注";
63
+ readonly 'form.environment': "环境";
64
+ readonly 'form.environmentHint': "如 development / production";
65
+ readonly 'form.tags': "标签";
66
+ readonly 'form.tagsHint': "逗号分隔";
67
+ readonly 'form.location': "位置";
68
+ readonly 'form.save': "保存";
69
+ readonly 'form.cancel': "取消";
70
+ readonly 'form.required': "请填写必填字段:别名、主机地址、用户名。";
71
+ readonly 'form.portInvalid': "端口必须是 1-65535 之间的整数。";
72
+ readonly 'terminal.selectHost': "选择主机";
73
+ readonly 'terminal.connect': "连接";
74
+ readonly 'terminal.disconnect': "断开";
75
+ readonly 'terminal.placeholder': "选择主机后点击「连接」打开远程终端。";
76
+ readonly 'terminal.connecting': "正在连接…";
77
+ readonly 'terminal.ready': "终端已连接({alias})";
78
+ readonly 'terminal.exited': "终端已退出({alias})";
79
+ readonly 'terminal.error': "终端错误:{error}";
80
+ readonly 'transfer.selectHost': "选择主机";
81
+ readonly 'transfer.remotePath': "远程路径";
82
+ readonly 'transfer.remotePathHint': "如 /tmp/ 或 /home/user/app.tar.gz";
83
+ readonly 'transfer.upload': "上传";
84
+ readonly 'transfer.uploading': "正在上传 {file}…";
85
+ readonly 'transfer.download': "下载";
86
+ readonly 'transfer.downloading': "正在下载…";
87
+ readonly 'transfer.done': "完成:{bytes} 字节";
88
+ readonly 'transfer.failed': "传输失败:{error}";
89
+ readonly 'transfer.browseRemote': "浏览远程目录";
90
+ readonly 'transfer.refresh': "刷新";
91
+ readonly 'transfer.name': "名称";
92
+ readonly 'transfer.upLevel': "上级目录";
93
+ readonly 'transfer.size': "大小";
94
+ readonly 'transfer.modified': "修改时间";
95
+ readonly 'transfer.bytes': "{value} B";
96
+ readonly 'transfer.kib': "{value} KB";
97
+ readonly 'transfer.mib': "{value} MB";
98
+ readonly 'transfer.gib': "{value} GB";
99
+ readonly 'transfer.percent': "{value}%";
100
+ readonly 'transfer.speed': "{value}/s";
101
+ readonly 'transfer.dir': "目录";
102
+ readonly 'transfer.file': "文件";
103
+ readonly 'tunnel.empty': "没有活跃的隧道。";
104
+ readonly 'tunnel.start': "新建隧道";
105
+ readonly 'tunnel.alias': "主机别名";
106
+ readonly 'tunnel.remotePort': "远程端口";
107
+ readonly 'tunnel.remoteHost': "远程主机";
108
+ readonly 'tunnel.remoteHostHint': "默认 127.0.0.1(服务器自身)";
109
+ readonly 'tunnel.localPort': "本地端口";
110
+ readonly 'tunnel.localPortHint': "留空自动分配";
111
+ readonly 'tunnel.stop': "停止";
112
+ readonly 'tunnel.stopAll': "全部停止";
113
+ readonly 'tunnel.stopAllConfirm': "确定停止全部隧道吗?";
114
+ readonly 'tunnel.row': "{alias}: 127.0.0.1:{localPort} -> {remoteHost}:{remotePort}";
115
+ readonly 'tunnel.started': "隧道已建立:127.0.0.1:{localPort}";
116
+ readonly 'tunnel.failed': "隧道启动失败:{error}";
117
+ readonly 'cluster.command': "命令";
118
+ readonly 'cluster.run': "执行";
119
+ readonly 'cluster.confirm': "确定在匹配的主机上执行该命令吗?该操作会真实运行在远程服务器上。";
120
+ readonly 'cluster.aliases': "主机别名(逗号分隔,留空为全部)";
121
+ readonly 'cluster.environment': "环境过滤";
122
+ readonly 'cluster.tags': "标签过滤(逗号分隔)";
123
+ readonly 'cluster.results': "执行结果";
124
+ readonly 'cluster.empty': "输入命令后点击「执行」。";
125
+ readonly 'cluster.noMatch': "没有匹配的主机。";
126
+ readonly 'cluster.ok': "成功";
127
+ readonly 'cluster.fail': "失败";
128
+ readonly 'cluster.timeout': "超时";
129
+ readonly 'cluster.col.alias': "主机";
130
+ readonly 'cluster.col.status': "状态";
131
+ readonly 'cluster.col.exit': "退出码";
132
+ readonly 'cluster.col.stdout': "标准输出";
133
+ readonly 'cluster.col.stderr': "标准错误";
134
+ readonly 'cluster.col.error': "错误";
135
+ readonly 'cluster.col.duration': "耗时";
136
+ readonly 'common.loading': "加载中…";
137
+ readonly 'common.error': "出错:{error}";
138
+ readonly 'common.refresh': "刷新";
139
+ readonly 'common.confirm': "确定";
132
140
  };
133
141
  export declare const en: Record<keyof typeof zh, string>;
134
142
  /** Locale key union. */
@@ -1,9 +1,25 @@
1
1
  import type { SshApi } from '../api.ts';
2
+ import type { SshHostSummary } from '../../protocol.ts';
2
3
  /** Hosts tab props. */
3
4
  export interface HostsTabProps {
4
5
  api: SshApi;
5
6
  /** Connect the given alias in the terminal tab. */
6
7
  onConnect: (alias: string) => void;
7
8
  }
9
+ /** Host list grouping modes (#379). */
10
+ export type HostGroupBy = 'none' | 'environment' | 'tags';
11
+ /** One collapsible group section of the grouped host list. */
12
+ export interface HostGroup {
13
+ /** Group key: the environment name, one tag, or '' for the ungrouped bucket. */
14
+ key: string;
15
+ hosts: SshHostSummary[];
16
+ }
17
+ /**
18
+ * Bucket hosts into collapsible groups (#379). Grouping by tags places a
19
+ * multi-tag host in every one of its tag groups (folder view); hosts without
20
+ * the grouping key land in the '' bucket, which always sorts last. Groups
21
+ * sort alphabetically; host order inside a group follows the API listing.
22
+ */
23
+ export declare function groupHosts(hosts: SshHostSummary[], groupBy: HostGroupBy): HostGroup[];
8
24
  /** The hosts table plus its toolbar and dialogs. */
9
25
  export declare function HostsTab({ api, onConnect }: HostsTabProps): import("react").JSX.Element;
@@ -164,7 +164,7 @@ export type TerminalClientFrame = {
164
164
  rows: number;
165
165
  };
166
166
  /** Route paths the client calls (shared literals). */
167
- export declare const SSH_API_BASE: '/api/dsh-ssh';
167
+ export declare const SSH_API_BASE: "/api/dsh-ssh";
168
168
  export declare const SSH_API: {
169
169
  readonly hosts: string;
170
170
  readonly importSshConfig: 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.1.20",
4
+ "version": "0.2.0",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": "^22.19.0 || >=24.0.0"
@@ -49,17 +49,17 @@
49
49
  },
50
50
  "devDependencies": {
51
51
  "@deepseek-ai/cordis": "^4.0.1",
52
- "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.6",
53
- "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6",
54
- "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
55
- "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.6",
56
- "@deepseek-ai/dsh-client-ui-sidebar": "^0.1.0-rc.6",
57
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
58
- "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
59
- "@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
60
- "@deepseek-ai/dsh-settings": "^0.1.0-rc.6",
61
- "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.6",
62
- "@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
52
+ "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.7",
53
+ "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.7",
54
+ "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.7",
55
+ "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.7",
56
+ "@deepseek-ai/dsh-client-ui-sidebar": "^0.1.0-rc.7",
57
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.7",
58
+ "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.7",
59
+ "@deepseek-ai/dsh-llm": "^0.1.0-rc.7",
60
+ "@deepseek-ai/dsh-settings": "^0.1.0-rc.7",
61
+ "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.7",
62
+ "@deepseek-ai/dsh-tools": "^0.1.0-rc.7",
63
63
  "@types/node": "^22.20.0",
64
64
  "@types/react": "~18.3.1",
65
65
  "@types/react-dom": "^18.3.5",
@@ -17,6 +17,14 @@ export const zh = {
17
17
  'hosts.add': '新增主机',
18
18
  'hosts.import': '导入 ~/.ssh/config',
19
19
  'hosts.search': '搜索别名 / 描述 / 标签…',
20
+ 'hosts.groupBy.label': '分组方式',
21
+ 'hosts.groupBy.none': '不分组',
22
+ 'hosts.groupBy.environment': '按环境分组',
23
+ 'hosts.groupBy.tags': '按标签分组',
24
+ 'hosts.group.ungrouped': '未设置环境',
25
+ 'hosts.group.noTags': '无标签',
26
+ 'hosts.group.count': '{count} 台',
27
+ 'hosts.group.testAll': '测试全部',
20
28
  'hosts.col.alias': '别名',
21
29
  'hosts.col.host': '主机',
22
30
  'hosts.col.user': '用户',
@@ -153,6 +161,14 @@ export const en: Record<keyof typeof zh, string> = {
153
161
  'hosts.add': 'Add host',
154
162
  'hosts.import': 'Import ~/.ssh/config',
155
163
  'hosts.search': 'Search alias / description / tags…',
164
+ 'hosts.groupBy.label': 'Group by',
165
+ 'hosts.groupBy.none': 'No grouping',
166
+ 'hosts.groupBy.environment': 'Group by environment',
167
+ 'hosts.groupBy.tags': 'Group by tags',
168
+ 'hosts.group.ungrouped': 'No environment',
169
+ 'hosts.group.noTags': 'No tags',
170
+ 'hosts.group.count': '{count} hosts',
171
+ 'hosts.group.testAll': 'Test all',
156
172
  'hosts.col.alias': 'Alias',
157
173
  'hosts.col.host': 'Host',
158
174
  'hosts.col.user': 'User',