@tiphareth/dsh-hardssh 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -0
- package/cordis.patch.yml +26 -0
- package/lib/client.js +17780 -0
- package/lib/environment-BL1jddfB.js +449 -0
- package/lib/fs.js +478 -0
- package/lib/index.js +6758 -0
- package/lib/subprocess.js +600 -0
- package/lib/switch-fs-CAJpFY9C.js +193 -0
- package/lib/switch-fs-RrZtG2gv.js +210 -0
- package/lib/types/backend.d.ts +108 -0
- package/lib/types/base/capability.d.ts +107 -0
- package/lib/types/base/index.d.ts +18 -0
- package/lib/types/base/ledger-router.d.ts +48 -0
- package/lib/types/base/ledger.d.ts +82 -0
- package/lib/types/base/model.d.ts +108 -0
- package/lib/types/base/namespace.d.ts +57 -0
- package/lib/types/base/plugin.d.ts +102 -0
- package/lib/types/base/registry.d.ts +60 -0
- package/lib/types/base/router.d.ts +40 -0
- package/lib/types/client/api.d.ts +85 -0
- package/lib/types/client/directory-flow.d.ts +82 -0
- package/lib/types/client/icons.d.ts +19 -0
- package/lib/types/client/index.d.ts +34 -0
- package/lib/types/client/locales.d.ts +104 -0
- package/lib/types/client/manager-button.d.ts +32 -0
- package/lib/types/client/migrate.d.ts +20 -0
- package/lib/types/client/ssh/api.d.ts +89 -0
- package/lib/types/client/ssh/apply.d.ts +23 -0
- package/lib/types/client/ssh/locales.d.ts +156 -0
- package/lib/types/client/ssh/mount.d.ts +13 -0
- package/lib/types/client/ssh/panel/ClusterTab.d.ts +8 -0
- package/lib/types/client/ssh/panel/HostFingerprintDialog.d.ts +16 -0
- package/lib/types/client/ssh/panel/HostFormDialog.d.ts +13 -0
- package/lib/types/client/ssh/panel/HostsTab.d.ts +10 -0
- package/lib/types/client/ssh/panel/SessionSecretDialog.d.ts +16 -0
- package/lib/types/client/ssh/panel/SshPanel.d.ts +14 -0
- package/lib/types/client/ssh/panel/TerminalTab.d.ts +12 -0
- package/lib/types/client/ssh/panel/TransferTab.d.ts +8 -0
- package/lib/types/client/ssh/panel/TunnelsTab.d.ts +8 -0
- package/lib/types/client/ssh/panel/controller.d.ts +23 -0
- package/lib/types/client/ssh/panel/helpers.d.ts +15 -0
- package/lib/types/client/ssh/panel/xterm.css.d.ts +3 -0
- package/lib/types/client/ssh/sidebar-entry.d.ts +25 -0
- package/lib/types/client/state.d.ts +32 -0
- package/lib/types/client/text.d.ts +11 -0
- package/lib/types/client/workspace-badges.d.ts +38 -0
- package/lib/types/client/workspace-gate.d.ts +15 -0
- package/lib/types/client-http.d.ts +15 -0
- package/lib/types/core.d.ts +43 -0
- package/lib/types/fs.d.ts +36 -0
- package/lib/types/host-http.d.ts +25 -0
- package/lib/types/index.d.ts +53 -0
- package/lib/types/ledger.d.ts +132 -0
- package/lib/types/protocol.d.ts +101 -0
- package/lib/types/providers/index.d.ts +19 -0
- package/lib/types/providers/local/provider.d.ts +58 -0
- package/lib/types/providers/ssh/provider.d.ts +112 -0
- package/lib/types/remote/environment.d.ts +29 -0
- package/lib/types/remote/output.d.ts +38 -0
- package/lib/types/remote/remote-fs.d.ts +69 -0
- package/lib/types/remote/remote-process.d.ts +41 -0
- package/lib/types/remote/remote-subprocess.d.ts +31 -0
- package/lib/types/remote/remote-terminal.d.ts +41 -0
- package/lib/types/remote-runner.d.ts +83 -0
- package/lib/types/remote-search.d.ts +50 -0
- package/lib/types/routes.d.ts +24 -0
- package/lib/types/runtime/workspace-core.d.ts +59 -0
- package/lib/types/seam-state.d.ts +69 -0
- package/lib/types/shell.d.ts +8 -0
- package/lib/types/ssh/connection/lease.d.ts +16 -0
- package/lib/types/ssh/connection/pool.d.ts +57 -0
- package/lib/types/ssh/engine.d.ts +434 -0
- package/lib/types/ssh/exec/output.d.ts +31 -0
- package/lib/types/ssh/known-hosts.d.ts +89 -0
- package/lib/types/ssh/plugin.d.ts +57 -0
- package/lib/types/ssh/protocol.d.ts +236 -0
- package/lib/types/ssh/routes.d.ts +44 -0
- package/lib/types/ssh/store.d.ts +107 -0
- package/lib/types/ssh/tools.d.ts +35 -0
- package/lib/types/ssh/transfer/progress.d.ts +14 -0
- package/lib/types/ssh/vault.d.ts +110 -0
- package/lib/types/subprocess.d.ts +32 -0
- package/lib/types/switch/switch-fs.d.ts +88 -0
- package/lib/types/switch/switch-subprocess.d.ts +34 -0
- package/lib/types/tools.d.ts +12 -0
- package/package.json +134 -0
- package/src/backend.ts +624 -0
- package/src/base/capability.ts +86 -0
- package/src/base/index.ts +18 -0
- package/src/base/ledger-router.ts +128 -0
- package/src/base/ledger.ts +299 -0
- package/src/base/model.ts +118 -0
- package/src/base/namespace.ts +102 -0
- package/src/base/plugin.ts +170 -0
- package/src/base/registry.ts +109 -0
- package/src/base/router.ts +43 -0
- package/src/client/api.ts +196 -0
- package/src/client/css-modules.d.ts +5 -0
- package/src/client/directory-flow.tsx +482 -0
- package/src/client/icons.tsx +50 -0
- package/src/client/index.ts +210 -0
- package/src/client/locales.ts +105 -0
- package/src/client/manager-button.tsx +269 -0
- package/src/client/migrate.ts +109 -0
- package/src/client/ssh/api.ts +411 -0
- package/src/client/ssh/apply.ts +50 -0
- package/src/client/ssh/locales.ts +322 -0
- package/src/client/ssh/mount.tsx +83 -0
- package/src/client/ssh/panel/ClusterTab.tsx +123 -0
- package/src/client/ssh/panel/HostFingerprintDialog.tsx +82 -0
- package/src/client/ssh/panel/HostFormDialog.tsx +228 -0
- package/src/client/ssh/panel/HostsTab.tsx +236 -0
- package/src/client/ssh/panel/SessionSecretDialog.tsx +80 -0
- package/src/client/ssh/panel/SshPanel.tsx +77 -0
- package/src/client/ssh/panel/TerminalTab.tsx +176 -0
- package/src/client/ssh/panel/TransferTab.tsx +232 -0
- package/src/client/ssh/panel/TunnelsTab.tsx +177 -0
- package/src/client/ssh/panel/controller.ts +48 -0
- package/src/client/ssh/panel/helpers.ts +26 -0
- package/src/client/ssh/panel/panel.module.css +1006 -0
- package/src/client/ssh/panel/xterm.css.ts +2 -0
- package/src/client/ssh/sidebar-entry.ts +123 -0
- package/src/client/state.ts +99 -0
- package/src/client/text.ts +22 -0
- package/src/client/workspace-badges.ts +96 -0
- package/src/client/workspace-gate.ts +225 -0
- package/src/client/workspace.module.css +283 -0
- package/src/client-http.ts +45 -0
- package/src/core.ts +47 -0
- package/src/fs.ts +85 -0
- package/src/host-http.ts +74 -0
- package/src/index.ts +244 -0
- package/src/ledger.ts +416 -0
- package/src/protocol.ts +114 -0
- package/src/providers/index.ts +34 -0
- package/src/providers/local/provider.ts +179 -0
- package/src/providers/ssh/provider.ts +274 -0
- package/src/remote/environment.ts +123 -0
- package/src/remote/output.ts +142 -0
- package/src/remote/remote-fs.ts +532 -0
- package/src/remote/remote-process.ts +203 -0
- package/src/remote/remote-subprocess.ts +159 -0
- package/src/remote/remote-terminal.ts +141 -0
- package/src/remote-runner.ts +201 -0
- package/src/remote-search.ts +163 -0
- package/src/routes.ts +395 -0
- package/src/runtime/workspace-core.ts +154 -0
- package/src/seam-state.ts +185 -0
- package/src/shell.ts +10 -0
- package/src/ssh/connection/lease.ts +17 -0
- package/src/ssh/connection/pool.ts +275 -0
- package/src/ssh/engine.ts +1761 -0
- package/src/ssh/exec/output.ts +70 -0
- package/src/ssh/known-hosts.ts +214 -0
- package/src/ssh/plugin.ts +184 -0
- package/src/ssh/protocol.ts +227 -0
- package/src/ssh/routes.ts +892 -0
- package/src/ssh/store.ts +544 -0
- package/src/ssh/tools.ts +402 -0
- package/src/ssh/transfer/progress.ts +75 -0
- package/src/ssh/vault.ts +477 -0
- package/src/subprocess.ts +71 -0
- package/src/switch/switch-fs.ts +253 -0
- package/src/switch/switch-subprocess.ts +59 -0
- package/src/tools.ts +221 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-ssh surface copy: zh is the key source, en mirrors every key.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const zh = {
|
|
6
|
+
'entry.label': 'SSH',
|
|
7
|
+
'entry.tooltip': '远程 SSH 运维面板',
|
|
8
|
+
'panel.title': '远程 SSH',
|
|
9
|
+
'tab.hosts': '主机',
|
|
10
|
+
'tab.terminal': '终端',
|
|
11
|
+
'tab.transfer': '传输',
|
|
12
|
+
'tab.tunnels': '隧道',
|
|
13
|
+
'tab.cluster': '集群',
|
|
14
|
+
// hosts
|
|
15
|
+
'hosts.empty': '尚未配置主机。点击「新增主机」添加,或从 ~/.ssh/config 导入。',
|
|
16
|
+
'hosts.add': '新增主机',
|
|
17
|
+
'hosts.import': '导入 ~/.ssh/config',
|
|
18
|
+
'hosts.search': '搜索别名 / 描述 / 标签…',
|
|
19
|
+
'hosts.col.alias': '别名',
|
|
20
|
+
'hosts.col.host': '主机',
|
|
21
|
+
'hosts.col.user': '用户',
|
|
22
|
+
'hosts.col.auth': '认证',
|
|
23
|
+
'hosts.col.environment': '环境',
|
|
24
|
+
'hosts.col.tags': '标签',
|
|
25
|
+
'hosts.col.description': '备注',
|
|
26
|
+
'hosts.col.actions': '操作',
|
|
27
|
+
'hosts.test': '测试',
|
|
28
|
+
'hosts.edit': '编辑',
|
|
29
|
+
'hosts.delete': '删除',
|
|
30
|
+
'hosts.deleteConfirm': '确定删除主机「{alias}」?将同时停止其隧道。',
|
|
31
|
+
'hosts.imported': '解析 {parsed} 个配置块,新增 {added} 台主机,跳过 {skipped} 个。',
|
|
32
|
+
'hosts.testing': '连接测试中…',
|
|
33
|
+
'hosts.testOk': '连接成功({latency} ms)',
|
|
34
|
+
'hosts.testFail': '连接失败:{error}',
|
|
35
|
+
'hosts.connected': '连接',
|
|
36
|
+
'hosts.notConnected': '未连接',
|
|
37
|
+
// host form
|
|
38
|
+
'form.title.create': '新增主机',
|
|
39
|
+
'form.title.edit': '编辑主机 {alias}',
|
|
40
|
+
'form.alias': '别名',
|
|
41
|
+
'form.aliasHint': '小写字母、数字与单连字符,操作时使用',
|
|
42
|
+
'form.host': '主机地址',
|
|
43
|
+
'form.port': '端口',
|
|
44
|
+
'form.user': '用户名',
|
|
45
|
+
'form.auth': '认证方式',
|
|
46
|
+
'form.auth.key': '密钥',
|
|
47
|
+
'form.auth.password': '密码',
|
|
48
|
+
'form.authKeepHint': '编辑时密钥/密码留空将保留原认证方式与已存凭据;如需更换认证方式请填写新凭据。',
|
|
49
|
+
'form.keyPath': '私钥路径',
|
|
50
|
+
'form.keyPathHint': '如 ~/.ssh/id_ed25519',
|
|
51
|
+
'form.agentHint': '留空则使用 ssh-agent 已加载的密钥:设置 SSH_AUTH_SOCK(Git-Bash ssh-agent / WSL / unix),零输入',
|
|
52
|
+
'form.passphrase': '密钥口令',
|
|
53
|
+
'form.password': '密码',
|
|
54
|
+
'form.passwordHint': '仅本次会话使用:首次连接时手动输入,不落盘(VSCode Remote-SSH 式)',
|
|
55
|
+
'form.proxyJump': '跳板机别名',
|
|
56
|
+
'form.proxyJumpHint': '逗号分隔的本地主机别名,按顺序连接',
|
|
57
|
+
'form.description': '备注',
|
|
58
|
+
'form.environment': '环境',
|
|
59
|
+
'form.environmentHint': '如 development / production',
|
|
60
|
+
'form.tags': '标签',
|
|
61
|
+
'form.tagsHint': '逗号分隔',
|
|
62
|
+
'form.location': '位置',
|
|
63
|
+
'form.save': '保存',
|
|
64
|
+
'form.cancel': '取消',
|
|
65
|
+
'form.required': '请填写必填字段:别名、主机地址、用户名。',
|
|
66
|
+
'form.portInvalid': '端口必须是 1-65535 之间的整数。',
|
|
67
|
+
// terminal
|
|
68
|
+
'terminal.selectHost': '选择主机',
|
|
69
|
+
'terminal.connect': '连接',
|
|
70
|
+
'terminal.disconnect': '断开',
|
|
71
|
+
'terminal.placeholder': '选择主机后点击「连接」打开远程终端。',
|
|
72
|
+
'terminal.connecting': '正在连接…',
|
|
73
|
+
'terminal.ready': '终端已连接({alias})',
|
|
74
|
+
'terminal.exited': '终端已退出({alias})',
|
|
75
|
+
'terminal.error': '终端错误:{error}',
|
|
76
|
+
// transfer
|
|
77
|
+
'transfer.selectHost': '选择主机',
|
|
78
|
+
'transfer.remotePath': '远程路径',
|
|
79
|
+
'transfer.remotePathHint': '如 /tmp/ 或 /home/user/app.tar.gz',
|
|
80
|
+
'transfer.upload': '上传',
|
|
81
|
+
'transfer.uploading': '正在上传 {file}…',
|
|
82
|
+
'transfer.download': '下载',
|
|
83
|
+
'transfer.downloading': '正在下载…',
|
|
84
|
+
'transfer.done': '完成:{bytes} 字节',
|
|
85
|
+
'transfer.failed': '传输失败:{error}',
|
|
86
|
+
'transfer.browseRemote': '浏览远程目录',
|
|
87
|
+
'transfer.refresh': '刷新',
|
|
88
|
+
'transfer.name': '名称',
|
|
89
|
+
'transfer.upLevel': '上级目录',
|
|
90
|
+
'transfer.size': '大小',
|
|
91
|
+
'transfer.modified': '修改时间',
|
|
92
|
+
'transfer.bytes': '{value} B',
|
|
93
|
+
'transfer.kib': '{value} KB',
|
|
94
|
+
'transfer.mib': '{value} MB',
|
|
95
|
+
'transfer.gib': '{value} GB',
|
|
96
|
+
'transfer.percent': '{value}%',
|
|
97
|
+
'transfer.speed': '{value}/s',
|
|
98
|
+
'transfer.dir': '目录',
|
|
99
|
+
'transfer.file': '文件',
|
|
100
|
+
// tunnels
|
|
101
|
+
'tunnel.empty': '没有活跃的隧道。',
|
|
102
|
+
'tunnel.start': '新建隧道',
|
|
103
|
+
'tunnel.alias': '主机别名',
|
|
104
|
+
'tunnel.remotePort': '远程端口',
|
|
105
|
+
'tunnel.remoteHost': '远程主机',
|
|
106
|
+
'tunnel.remoteHostHint': '默认 127.0.0.1(服务器自身)',
|
|
107
|
+
'tunnel.localPort': '本地端口',
|
|
108
|
+
'tunnel.localPortHint': '留空自动分配',
|
|
109
|
+
'tunnel.stop': '停止',
|
|
110
|
+
'tunnel.stopAll': '全部停止',
|
|
111
|
+
'tunnel.stopAllConfirm': '确定停止全部隧道吗?',
|
|
112
|
+
'tunnel.row': '{alias}: 127.0.0.1:{localPort} -> {remoteHost}:{remotePort}',
|
|
113
|
+
'tunnel.started': '隧道已建立:127.0.0.1:{localPort}',
|
|
114
|
+
'tunnel.failed': '隧道启动失败:{error}',
|
|
115
|
+
// cluster
|
|
116
|
+
'cluster.command': '命令',
|
|
117
|
+
'cluster.run': '执行',
|
|
118
|
+
'cluster.confirm': '确定在匹配的主机上执行该命令吗?该操作会真实运行在远程服务器上。',
|
|
119
|
+
'cluster.aliases': '主机别名(逗号分隔,留空为全部)',
|
|
120
|
+
'cluster.environment': '环境过滤',
|
|
121
|
+
'cluster.tags': '标签过滤(逗号分隔)',
|
|
122
|
+
'cluster.results': '执行结果',
|
|
123
|
+
'cluster.empty': '输入命令后点击「执行」。',
|
|
124
|
+
'cluster.noMatch': '没有匹配的主机。',
|
|
125
|
+
'cluster.ok': '成功',
|
|
126
|
+
'cluster.fail': '失败',
|
|
127
|
+
'cluster.timeout': '超时',
|
|
128
|
+
'cluster.col.alias': '主机',
|
|
129
|
+
'cluster.col.status': '状态',
|
|
130
|
+
'cluster.col.exit': '退出码',
|
|
131
|
+
'cluster.col.stdout': '标准输出',
|
|
132
|
+
'cluster.col.stderr': '标准错误',
|
|
133
|
+
'cluster.col.error': '错误',
|
|
134
|
+
'cluster.col.duration': '耗时',
|
|
135
|
+
// common
|
|
136
|
+
'common.loading': '加载中…',
|
|
137
|
+
'common.error': '出错:{error}',
|
|
138
|
+
'common.refresh': '刷新',
|
|
139
|
+
'common.close': '关闭',
|
|
140
|
+
'common.confirm': '确定',
|
|
141
|
+
// host fingerprint (TOFU)
|
|
142
|
+
'fingerprint.title': '确认主机密钥 · {alias}',
|
|
143
|
+
'fingerprint.titleMismatch': '主机密钥变化 · {alias}',
|
|
144
|
+
'fingerprint.intro': '首次连接该服务器。请通过独立可信渠道核对其密钥指纹,确认无误后再信任:',
|
|
145
|
+
'fingerprint.mismatchIntro': '该服务器的密钥与之前记录的不一致,可能存在中间人攻击或服务器密钥轮换:',
|
|
146
|
+
'fingerprint.mismatchWarn': '若确认服务器正常(如重装系统/轮换密钥),请先「重置」再重新信任。',
|
|
147
|
+
'fingerprint.cancel': '取消',
|
|
148
|
+
'fingerprint.trust': '信任',
|
|
149
|
+
'fingerprint.forget': '重置',
|
|
150
|
+
// session secret (VSCode Remote-SSH style)
|
|
151
|
+
'sessionSecret.title': '请输入 {account} 的密码',
|
|
152
|
+
'sessionSecret.titlePassphrase': '请输入 {account} 的密钥口令',
|
|
153
|
+
'sessionSecret.intro': '',
|
|
154
|
+
'sessionSecret.introPassphrase': '',
|
|
155
|
+
'sessionSecret.placeholder': '密码',
|
|
156
|
+
'sessionSecret.placeholderPassphrase': '密钥口令',
|
|
157
|
+
'sessionSecret.required': '请输入后再连接',
|
|
158
|
+
'sessionSecret.connect': '连接',
|
|
159
|
+
'sessionSecret.cancel': '取消',
|
|
160
|
+
} as const
|
|
161
|
+
|
|
162
|
+
export const en: Record<keyof typeof zh, string> = {
|
|
163
|
+
'entry.label': 'SSH',
|
|
164
|
+
'entry.tooltip': 'Remote SSH operations panel',
|
|
165
|
+
'panel.title': 'Remote SSH',
|
|
166
|
+
'tab.hosts': 'Hosts',
|
|
167
|
+
'tab.terminal': 'Terminal',
|
|
168
|
+
'tab.transfer': 'Transfer',
|
|
169
|
+
'tab.tunnels': 'Tunnels',
|
|
170
|
+
'tab.cluster': 'Cluster',
|
|
171
|
+
'hosts.empty': 'No hosts configured. Click "Add host" or import from ~/.ssh/config.',
|
|
172
|
+
'hosts.add': 'Add host',
|
|
173
|
+
'hosts.import': 'Import ~/.ssh/config',
|
|
174
|
+
'hosts.search': 'Search alias / description / tags…',
|
|
175
|
+
'hosts.col.alias': 'Alias',
|
|
176
|
+
'hosts.col.host': 'Host',
|
|
177
|
+
'hosts.col.user': 'User',
|
|
178
|
+
'hosts.col.auth': 'Auth',
|
|
179
|
+
'hosts.col.environment': 'Environment',
|
|
180
|
+
'hosts.col.tags': 'Tags',
|
|
181
|
+
'hosts.col.description': 'Description',
|
|
182
|
+
'hosts.col.actions': 'Actions',
|
|
183
|
+
'hosts.test': 'Test',
|
|
184
|
+
'hosts.edit': 'Edit',
|
|
185
|
+
'hosts.delete': 'Delete',
|
|
186
|
+
'hosts.deleteConfirm': 'Delete host "{alias}"? Its tunnels will be stopped.',
|
|
187
|
+
'hosts.imported': 'Parsed {parsed} blocks, added {added} hosts, skipped {skipped}.',
|
|
188
|
+
'hosts.testing': 'Testing connection…',
|
|
189
|
+
'hosts.testOk': 'Connected ({latency} ms)',
|
|
190
|
+
'hosts.testFail': 'Connection failed: {error}',
|
|
191
|
+
'hosts.connected': 'Connect',
|
|
192
|
+
'hosts.notConnected': 'Not connected',
|
|
193
|
+
'form.title.create': 'Add host',
|
|
194
|
+
'form.title.edit': 'Edit host {alias}',
|
|
195
|
+
'form.alias': 'Alias',
|
|
196
|
+
'form.aliasHint': 'Lowercase letters, digits and single hyphens; used in every operation',
|
|
197
|
+
'form.host': 'Host',
|
|
198
|
+
'form.port': 'Port',
|
|
199
|
+
'form.user': 'User',
|
|
200
|
+
'form.auth': 'Authentication',
|
|
201
|
+
'form.auth.key': 'Key',
|
|
202
|
+
'form.auth.password': 'Password',
|
|
203
|
+
'form.authKeepHint': 'While editing, empty secret fields keep the stored authentication and credentials; enter new secrets to switch methods.',
|
|
204
|
+
'form.keyPath': 'Private key path',
|
|
205
|
+
'form.keyPathHint': 'e.g. ~/.ssh/id_ed25519',
|
|
206
|
+
'form.agentHint': 'Leave empty to use keys from ssh-agent: set SSH_AUTH_SOCK (unix / Git-Bash ssh-agent / WSL) — zero input',
|
|
207
|
+
'form.passphrase': 'Key passphrase',
|
|
208
|
+
'form.password': 'Password',
|
|
209
|
+
'form.passwordHint': 'Session-only: enter on first connect, never persisted (VSCode Remote-SSH style)',
|
|
210
|
+
'form.proxyJump': 'Jump host aliases',
|
|
211
|
+
'form.proxyJumpHint': 'Comma-separated local aliases, chained in order',
|
|
212
|
+
'form.description': 'Description',
|
|
213
|
+
'form.environment': 'Environment',
|
|
214
|
+
'form.environmentHint': 'e.g. development / production',
|
|
215
|
+
'form.tags': 'Tags',
|
|
216
|
+
'form.tagsHint': 'Comma-separated',
|
|
217
|
+
'form.location': 'Location',
|
|
218
|
+
'form.save': 'Save',
|
|
219
|
+
'form.cancel': 'Cancel',
|
|
220
|
+
'form.required': 'Required fields missing: alias, host, user.',
|
|
221
|
+
'form.portInvalid': 'Port must be an integer between 1 and 65535.',
|
|
222
|
+
'terminal.selectHost': 'Select host',
|
|
223
|
+
'terminal.connect': 'Connect',
|
|
224
|
+
'terminal.disconnect': 'Disconnect',
|
|
225
|
+
'terminal.placeholder': 'Select a host and click "Connect" to open the remote terminal.',
|
|
226
|
+
'terminal.connecting': 'Connecting…',
|
|
227
|
+
'terminal.ready': 'Terminal connected ({alias})',
|
|
228
|
+
'terminal.exited': 'Terminal exited ({alias})',
|
|
229
|
+
'terminal.error': 'Terminal error: {error}',
|
|
230
|
+
'transfer.selectHost': 'Select host',
|
|
231
|
+
'transfer.remotePath': 'Remote path',
|
|
232
|
+
'transfer.remotePathHint': 'e.g. /tmp/ or /home/user/app.tar.gz',
|
|
233
|
+
'transfer.upload': 'Upload',
|
|
234
|
+
'transfer.uploading': 'Uploading {file}…',
|
|
235
|
+
'transfer.download': 'Download',
|
|
236
|
+
'transfer.downloading': 'Downloading…',
|
|
237
|
+
'transfer.done': 'Done: {bytes} bytes',
|
|
238
|
+
'transfer.failed': 'Transfer failed: {error}',
|
|
239
|
+
'transfer.browseRemote': 'Browse remote',
|
|
240
|
+
'transfer.refresh': 'Refresh',
|
|
241
|
+
'transfer.name': 'Name',
|
|
242
|
+
'transfer.upLevel': 'Parent directory',
|
|
243
|
+
'transfer.size': 'Size',
|
|
244
|
+
'transfer.modified': 'Modified',
|
|
245
|
+
'transfer.bytes': '{value} B',
|
|
246
|
+
'transfer.kib': '{value} KB',
|
|
247
|
+
'transfer.mib': '{value} MB',
|
|
248
|
+
'transfer.gib': '{value} GB',
|
|
249
|
+
'transfer.percent': '{value}%',
|
|
250
|
+
'transfer.speed': '{value}/s',
|
|
251
|
+
'transfer.dir': 'Directory',
|
|
252
|
+
'transfer.file': 'File',
|
|
253
|
+
'tunnel.empty': 'No active tunnels.',
|
|
254
|
+
'tunnel.start': 'New tunnel',
|
|
255
|
+
'tunnel.alias': 'Host alias',
|
|
256
|
+
'tunnel.remotePort': 'Remote port',
|
|
257
|
+
'tunnel.remoteHost': 'Remote host',
|
|
258
|
+
'tunnel.remoteHostHint': 'Default 127.0.0.1 (the server itself)',
|
|
259
|
+
'tunnel.localPort': 'Local port',
|
|
260
|
+
'tunnel.localPortHint': 'Leave empty for auto-assign',
|
|
261
|
+
'tunnel.stop': 'Stop',
|
|
262
|
+
'tunnel.stopAll': 'Stop all',
|
|
263
|
+
'tunnel.stopAllConfirm': 'Stop all tunnels?',
|
|
264
|
+
'tunnel.row': '{alias}: 127.0.0.1:{localPort} -> {remoteHost}:{remotePort}',
|
|
265
|
+
'tunnel.started': 'Tunnel up: 127.0.0.1:{localPort}',
|
|
266
|
+
'tunnel.failed': 'Tunnel failed: {error}',
|
|
267
|
+
'cluster.command': 'Command',
|
|
268
|
+
'cluster.run': 'Run',
|
|
269
|
+
'cluster.confirm': 'Run this command on the matching hosts? It executes for real on the remote servers.',
|
|
270
|
+
'cluster.aliases': 'Host aliases (comma-separated; empty = all)',
|
|
271
|
+
'cluster.environment': 'Environment filter',
|
|
272
|
+
'cluster.tags': 'Tag filter (comma-separated)',
|
|
273
|
+
'cluster.results': 'Results',
|
|
274
|
+
'cluster.empty': 'Type a command and click "Run".',
|
|
275
|
+
'cluster.noMatch': 'No hosts matched.',
|
|
276
|
+
'cluster.ok': 'OK',
|
|
277
|
+
'cluster.fail': 'Failed',
|
|
278
|
+
'cluster.timeout': 'Timed out',
|
|
279
|
+
'cluster.col.alias': 'Host',
|
|
280
|
+
'cluster.col.status': 'Status',
|
|
281
|
+
'cluster.col.exit': 'Exit',
|
|
282
|
+
'cluster.col.stdout': 'Stdout',
|
|
283
|
+
'cluster.col.stderr': 'Stderr',
|
|
284
|
+
'cluster.col.error': 'Error',
|
|
285
|
+
'cluster.col.duration': 'Duration',
|
|
286
|
+
'common.loading': 'Loading…',
|
|
287
|
+
'common.error': 'Error: {error}',
|
|
288
|
+
'common.refresh': 'Refresh',
|
|
289
|
+
'common.close': 'Close',
|
|
290
|
+
'common.confirm': 'Confirm',
|
|
291
|
+
'fingerprint.title': 'Confirm host key · {alias}',
|
|
292
|
+
'fingerprint.titleMismatch': 'Host key changed · {alias}',
|
|
293
|
+
'fingerprint.intro': 'First time connecting to this server. Verify the key fingerprint through an independent trusted channel, then confirm:',
|
|
294
|
+
'fingerprint.mismatchIntro': 'The server key differs from the previously recorded one — possible man-in-the-middle or key rotation:',
|
|
295
|
+
'fingerprint.mismatchWarn': 'If the server is legitimate (e.g. reinstall / key rotation), reset first, then re-trust.',
|
|
296
|
+
'fingerprint.cancel': 'Cancel',
|
|
297
|
+
'fingerprint.trust': 'Trust',
|
|
298
|
+
'fingerprint.forget': 'Reset',
|
|
299
|
+
'sessionSecret.title': 'Enter the password for {account}',
|
|
300
|
+
'sessionSecret.titlePassphrase': 'Enter the passphrase for {account}',
|
|
301
|
+
'sessionSecret.intro': '',
|
|
302
|
+
'sessionSecret.introPassphrase': '',
|
|
303
|
+
'sessionSecret.placeholder': 'Password',
|
|
304
|
+
'sessionSecret.placeholderPassphrase': 'Passphrase',
|
|
305
|
+
'sessionSecret.required': 'Enter a value first',
|
|
306
|
+
'sessionSecret.connect': 'Connect',
|
|
307
|
+
'sessionSecret.cancel': 'Cancel',
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Locale key union. */
|
|
311
|
+
export type SshKey = keyof typeof zh
|
|
312
|
+
|
|
313
|
+
/** Tiny interpolation: {name} -> value. */
|
|
314
|
+
export function t(dictionary: Record<string, string>, key: string, values?: Record<string, string | number>): string {
|
|
315
|
+
let text = dictionary[key] ?? key
|
|
316
|
+
if (values !== undefined) {
|
|
317
|
+
for (const [name, value] of Object.entries(values)) {
|
|
318
|
+
text = text.replaceAll(`{${name}}`, String(value))
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return text
|
|
322
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Panel view mounting.
|
|
3
|
+
*
|
|
4
|
+
* The `conversation` slot is single-occupant (ui-conversation) and external
|
|
5
|
+
* plugins cannot declare slots, so the panel takes over the center column at
|
|
6
|
+
* the DOM level: a container is appended inside the `[data-pane="conversation"]`
|
|
7
|
+
* grid item (an extra trailing child React never manages), and a stylesheet
|
|
8
|
+
* rule hides the conversation content while the panel is active. Toggling is
|
|
9
|
+
* a data attribute on <html> — no React involvement, so the conversation
|
|
10
|
+
* subtree underneath stays mounted and stateful.
|
|
11
|
+
*/
|
|
12
|
+
import { createRoot, type Root } from 'react-dom/client'
|
|
13
|
+
import type { SshApi } from './api.ts'
|
|
14
|
+
import type { PanelController } from './panel/controller.ts'
|
|
15
|
+
import { SshPanel } from './panel/SshPanel.tsx'
|
|
16
|
+
import css from './panel/panel.module.css'
|
|
17
|
+
|
|
18
|
+
/** The injected panel container (kept in the DOM, hidden when inactive). */
|
|
19
|
+
export const PANEL_VIEW_SELECTOR = '[data-dsh-ssh-view]'
|
|
20
|
+
|
|
21
|
+
const CONVERSATION_COLUMN_SELECTOR = '[data-pane="conversation"]'
|
|
22
|
+
const ACTIVE_ATTR = 'data-dsh-ssh-active'
|
|
23
|
+
|
|
24
|
+
/** Find the center column, or undefined while the frame is not mounted. */
|
|
25
|
+
function conversationColumn(): HTMLElement | undefined {
|
|
26
|
+
return document.querySelector<HTMLElement>(CONVERSATION_COLUMN_SELECTOR) ?? undefined
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Mount the panel React tree into the center column and bind its visibility
|
|
31
|
+
* to the controller's panelOpen state.
|
|
32
|
+
* @param controller - the panel controller driving the view.
|
|
33
|
+
* @param api - the SSH API client the tabs operate through.
|
|
34
|
+
* @returns disposer unmounting the tree and restoring the column.
|
|
35
|
+
*/
|
|
36
|
+
export function mountPanel(controller: PanelController, api: SshApi): () => void {
|
|
37
|
+
let root: Root | undefined
|
|
38
|
+
let container: HTMLDivElement | undefined
|
|
39
|
+
|
|
40
|
+
const ensure = (): void => {
|
|
41
|
+
if (container !== undefined) {
|
|
42
|
+
if (container.isConnected) return
|
|
43
|
+
// The conversation pane was replaced; drop the stale tree and remount.
|
|
44
|
+
root?.unmount()
|
|
45
|
+
root = undefined
|
|
46
|
+
container.remove()
|
|
47
|
+
container = undefined
|
|
48
|
+
}
|
|
49
|
+
const column = conversationColumn()
|
|
50
|
+
if (column === undefined) return
|
|
51
|
+
container = document.createElement('div')
|
|
52
|
+
container.dataset.dshSshView = ''
|
|
53
|
+
container.className = css.view
|
|
54
|
+
column.appendChild(container)
|
|
55
|
+
root = createRoot(container)
|
|
56
|
+
root.render(<SshPanel controller={controller} api={api} />)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// The frame mounts after boot settlement; watch for the column's arrival.
|
|
60
|
+
const waitObserver = new MutationObserver(() => { ensure() })
|
|
61
|
+
waitObserver.observe(document.body, { childList: true, subtree: true })
|
|
62
|
+
|
|
63
|
+
const applyActive = (): void => {
|
|
64
|
+
if (controller.getSnapshot().panelOpen) {
|
|
65
|
+
document.documentElement.setAttribute(ACTIVE_ATTR, '')
|
|
66
|
+
} else {
|
|
67
|
+
document.documentElement.removeAttribute(ACTIVE_ATTR)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const unsubscribe = controller.subscribe(applyActive)
|
|
71
|
+
applyActive()
|
|
72
|
+
ensure()
|
|
73
|
+
|
|
74
|
+
return () => {
|
|
75
|
+
waitObserver.disconnect()
|
|
76
|
+
unsubscribe()
|
|
77
|
+
document.documentElement.removeAttribute(ACTIVE_ATTR)
|
|
78
|
+
root?.unmount()
|
|
79
|
+
root = undefined
|
|
80
|
+
container?.remove()
|
|
81
|
+
container = undefined
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cluster tab: run one command across many hosts (aliases / environment /
|
|
3
|
+
* tags filters) and inspect per-host results. Stdout/stderr render in
|
|
4
|
+
* collapsed <details> blocks; status renders as a colored badge.
|
|
5
|
+
*/
|
|
6
|
+
import { useState } from 'react'
|
|
7
|
+
import type { SshApi } from '../api.ts'
|
|
8
|
+
import type { ClusterResult } from '../../../ssh/protocol.ts'
|
|
9
|
+
import { errorMessage, tt } from './helpers.ts'
|
|
10
|
+
import css from './panel.module.css'
|
|
11
|
+
|
|
12
|
+
/** Cluster tab props. */
|
|
13
|
+
export interface ClusterTabProps {
|
|
14
|
+
api: SshApi
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Split a comma-separated input into a trimmed, non-empty string list. */
|
|
18
|
+
function splitList(text: string): string[] {
|
|
19
|
+
return text.split(',').map(part => part.trim()).filter(part => part !== '')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The cluster execution tab. */
|
|
23
|
+
export function ClusterTab({ api }: ClusterTabProps) {
|
|
24
|
+
const [command, setCommand] = useState('')
|
|
25
|
+
const [aliases, setAliases] = useState('')
|
|
26
|
+
const [environment, setEnvironment] = useState('')
|
|
27
|
+
const [tags, setTags] = useState('')
|
|
28
|
+
const [running, setRunning] = useState(false)
|
|
29
|
+
const [results, setResults] = useState<ClusterResult[] | null>(null)
|
|
30
|
+
const [error, setError] = useState<string | null>(null)
|
|
31
|
+
|
|
32
|
+
const run = async (): Promise<void> => {
|
|
33
|
+
if (command.trim() === '' || running) return
|
|
34
|
+
if (!window.confirm(tt('cluster.confirm'))) return
|
|
35
|
+
setRunning(true)
|
|
36
|
+
setError(null)
|
|
37
|
+
try {
|
|
38
|
+
const aliasList = splitList(aliases)
|
|
39
|
+
const tagList = splitList(tags)
|
|
40
|
+
const outcome = await api.cluster({
|
|
41
|
+
command: command.trim(),
|
|
42
|
+
aliases: aliasList.length > 0 ? aliasList : undefined,
|
|
43
|
+
environment: environment.trim() === '' ? undefined : environment.trim(),
|
|
44
|
+
tags: tagList.length > 0 ? tagList : undefined,
|
|
45
|
+
})
|
|
46
|
+
setResults(outcome)
|
|
47
|
+
} catch (cause) {
|
|
48
|
+
setError(errorMessage(cause))
|
|
49
|
+
} finally {
|
|
50
|
+
setRunning(false)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div className={css.fillBody}>
|
|
56
|
+
<div className={css.clusterForm}>
|
|
57
|
+
<label className={css.field}>
|
|
58
|
+
<span className={css.fieldLabel}>{tt('cluster.command')}</span>
|
|
59
|
+
<textarea className={css.input + ' ' + css.commandInput} value={command} onChange={event => { setCommand(event.target.value) }} />
|
|
60
|
+
</label>
|
|
61
|
+
<div className={css.clusterFilters}>
|
|
62
|
+
<input className={css.input} placeholder={tt('cluster.aliases')} value={aliases} onChange={event => { setAliases(event.target.value) }} />
|
|
63
|
+
<input className={css.input} placeholder={tt('cluster.environment')} value={environment} onChange={event => { setEnvironment(event.target.value) }} />
|
|
64
|
+
<input className={css.input} placeholder={tt('cluster.tags')} value={tags} onChange={event => { setTags(event.target.value) }} />
|
|
65
|
+
</div>
|
|
66
|
+
<div>
|
|
67
|
+
<button type="button" className={css.primaryButton} disabled={running || command.trim() === ''} onClick={() => { void run() }}>{tt('cluster.run')}</button>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
{error !== null && <div className={css.banner} data-kind="error">{tt('common.error', { error })}</div>}
|
|
71
|
+
{results === null && error === null && <div className={css.empty}>{tt('cluster.empty')}</div>}
|
|
72
|
+
{results !== null && results.length === 0 && <div className={css.empty}>{tt('cluster.noMatch')}</div>}
|
|
73
|
+
{results !== null && results.length > 0 && (
|
|
74
|
+
<div className={css.tableWrap}>
|
|
75
|
+
<table className={css.table}>
|
|
76
|
+
<thead>
|
|
77
|
+
<tr>
|
|
78
|
+
<th>{tt('cluster.col.alias')}</th>
|
|
79
|
+
<th>{tt('cluster.col.status')}</th>
|
|
80
|
+
<th>{tt('cluster.col.exit')}</th>
|
|
81
|
+
<th>{tt('cluster.col.stdout')}</th>
|
|
82
|
+
<th>{tt('cluster.col.stderr')}</th>
|
|
83
|
+
<th>{tt('cluster.col.error')}</th>
|
|
84
|
+
<th>{tt('cluster.col.duration')}</th>
|
|
85
|
+
</tr>
|
|
86
|
+
</thead>
|
|
87
|
+
<tbody>
|
|
88
|
+
{results.map(result => {
|
|
89
|
+
const status = result.ok ? 'ok' : result.timedOut === true ? 'timeout' : 'fail'
|
|
90
|
+
const label = result.ok ? tt('cluster.ok') : result.timedOut === true ? tt('cluster.timeout') : tt('cluster.fail')
|
|
91
|
+
return (
|
|
92
|
+
<tr key={result.alias}>
|
|
93
|
+
<td className={css.mono}>{result.alias}</td>
|
|
94
|
+
<td><span className={css.badge} data-status={status}>{label}</span></td>
|
|
95
|
+
<td className={css.mono}>{result.exitCode ?? '-'}</td>
|
|
96
|
+
<td>
|
|
97
|
+
{result.stdout !== undefined && result.stdout !== '' && (
|
|
98
|
+
<details className={css.cellDetails}>
|
|
99
|
+
<summary>{tt('cluster.col.stdout')}</summary>
|
|
100
|
+
<pre className={css.cellPre}>{result.stdout}</pre>
|
|
101
|
+
</details>
|
|
102
|
+
)}
|
|
103
|
+
</td>
|
|
104
|
+
<td>
|
|
105
|
+
{result.stderr !== undefined && result.stderr !== '' && (
|
|
106
|
+
<details className={css.cellDetails}>
|
|
107
|
+
<summary>{tt('cluster.col.stderr')}</summary>
|
|
108
|
+
<pre className={css.cellPre}>{result.stderr}</pre>
|
|
109
|
+
</details>
|
|
110
|
+
)}
|
|
111
|
+
</td>
|
|
112
|
+
<td className={css.cellMuted}>{result.error ?? ''}</td>
|
|
113
|
+
<td className={css.mono}>{result.durationMs !== undefined ? result.durationMs + ' ms' : '-'}</td>
|
|
114
|
+
</tr>
|
|
115
|
+
)
|
|
116
|
+
})}
|
|
117
|
+
</tbody>
|
|
118
|
+
</table>
|
|
119
|
+
</div>
|
|
120
|
+
)}
|
|
121
|
+
</div>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host fingerprint confirm dialog: shown when a connection is refused because
|
|
3
|
+
* the server's host key is not trusted yet (TOFU first encounter) or changed
|
|
4
|
+
* (possible MITM / key rotation). Displays the canonical SHA256 fingerprint
|
|
5
|
+
* and lets the operator confirm (trust) or reset (forget) the record.
|
|
6
|
+
*/
|
|
7
|
+
import { useState } from 'react'
|
|
8
|
+
import type { SshApi } from '../api.ts'
|
|
9
|
+
import { tt } from './helpers.ts'
|
|
10
|
+
import css from './panel.module.css'
|
|
11
|
+
|
|
12
|
+
/** The dialog's props. */
|
|
13
|
+
export interface HostFingerprintDialogProps {
|
|
14
|
+
api: SshApi
|
|
15
|
+
alias: string
|
|
16
|
+
/** The server's canonical fingerprint (SHA256:…). */
|
|
17
|
+
fingerprintSha256: string
|
|
18
|
+
/** True when the refused connection reported a MISMATCH (key changed). */
|
|
19
|
+
mismatch: boolean
|
|
20
|
+
onClose: () => void
|
|
21
|
+
/** Called after a successful trust (retry the connection). */
|
|
22
|
+
onTrusted: () => void
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Confirm (or reset) one host key. */
|
|
26
|
+
export function HostFingerprintDialog({ api, alias, fingerprintSha256, mismatch, onClose, onTrusted }: HostFingerprintDialogProps) {
|
|
27
|
+
const [busy, setBusy] = useState(false)
|
|
28
|
+
const [error, setError] = useState<string | null>(null)
|
|
29
|
+
|
|
30
|
+
const trust = async (): Promise<void> => {
|
|
31
|
+
setBusy(true)
|
|
32
|
+
setError(null)
|
|
33
|
+
try {
|
|
34
|
+
await api.hostKeyAction(alias, 'trust')
|
|
35
|
+
onTrusted()
|
|
36
|
+
} catch (cause) {
|
|
37
|
+
setError(cause instanceof Error ? cause.message : String(cause))
|
|
38
|
+
setBusy(false)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const forget = async (): Promise<void> => {
|
|
43
|
+
setBusy(true)
|
|
44
|
+
setError(null)
|
|
45
|
+
try {
|
|
46
|
+
await api.hostKeyAction(alias, 'forget')
|
|
47
|
+
onTrusted()
|
|
48
|
+
} catch (cause) {
|
|
49
|
+
setError(cause instanceof Error ? cause.message : String(cause))
|
|
50
|
+
setBusy(false)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div className={css.modalBackdrop} onClick={onClose}>
|
|
56
|
+
<div className={css.modal} role="dialog" aria-modal="true" onClick={event => { event.stopPropagation() }}>
|
|
57
|
+
<div className={css.modalHeader}>
|
|
58
|
+
<h3 className={css.modalTitle}>{mismatch ? tt('fingerprint.titleMismatch', { alias }) : tt('fingerprint.title', { alias })}</h3>
|
|
59
|
+
</div>
|
|
60
|
+
<div className={css.modalBody}>
|
|
61
|
+
<p className={css.fingerprintIntro}>{tt(mismatch ? 'fingerprint.mismatchIntro' : 'fingerprint.intro')}</p>
|
|
62
|
+
<code className={css.fingerprintCode}>{fingerprintSha256}</code>
|
|
63
|
+
{mismatch && (
|
|
64
|
+
<p className={css.fingerprintWarn}>{tt('fingerprint.mismatchWarn')}</p>
|
|
65
|
+
)}
|
|
66
|
+
{error !== null && <p className={css.formError}>{error}</p>}
|
|
67
|
+
</div>
|
|
68
|
+
<div className={css.modalFooter}>
|
|
69
|
+
<button type="button" className={css.modalGhost} disabled={busy} onClick={onClose}>{tt('fingerprint.cancel')}</button>
|
|
70
|
+
{mismatch && (
|
|
71
|
+
<button type="button" className={css.modalGhost} disabled={busy} onClick={() => { void forget() }}>
|
|
72
|
+
{tt('fingerprint.forget')}
|
|
73
|
+
</button>
|
|
74
|
+
)}
|
|
75
|
+
<button type="button" className={css.modalPrimary} disabled={busy} onClick={() => { void trust() }}>
|
|
76
|
+
{tt('fingerprint.trust')}
|
|
77
|
+
</button>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
)
|
|
82
|
+
}
|