@pyrokine/mcp-ssh 1.0.0 → 1.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.
@@ -0,0 +1,264 @@
1
+ /**
2
+ * SSH Config Parser
3
+ *
4
+ * 解析 ~/.ssh/config 文件,提取 Host 配置
5
+ * 支持:
6
+ * - Host 多别名(Host a b c)
7
+ * - Host * 全局默认配置继承
8
+ * - ProxyJump 解析(支持 user@host:port 格式)
9
+ */
10
+
11
+ import * as fs from 'fs'
12
+ import * as os from 'os'
13
+ import * as path from 'path'
14
+
15
+ export interface SSHConfigHost {
16
+ host: string; // Host 别名
17
+ hostName?: string; // 实际地址
18
+ user?: string; // 用户名
19
+ port?: number; // 端口
20
+ identityFile?: string; // 私钥路径
21
+ proxyJump?: string; // 跳板机(原始字符串,可能是 user@host:port 格式)
22
+ }
23
+
24
+ /** ProxyJump 解析结果 */
25
+ export interface ParsedProxyJump {
26
+ user?: string;
27
+ host: string;
28
+ port?: number;
29
+ }
30
+
31
+ /** 内部使用的配置块 */
32
+ interface ConfigBlock {
33
+ patterns: string[]; // Host 行的所有模式/别名
34
+ config: Omit<SSHConfigHost, 'host'>;
35
+ }
36
+
37
+ /**
38
+ * 解析 host:port 或 [ipv6]:port
39
+ * 返回 { host, port },host 不含方括号
40
+ */
41
+ function parseHostPort(s: string): { host: string; port?: number } {
42
+ // IPv6 方括号格式: [addr]:port 或 [addr]
43
+ if (s.startsWith('[')) {
44
+ const closeBracket = s.indexOf(']')
45
+ if (closeBracket !== -1) {
46
+ const host = s.slice(1, closeBracket) // 去掉方括号
47
+ const rest = s.slice(closeBracket + 1)
48
+ if (rest.startsWith(':')) {
49
+ const parsedPort = parseInt(rest.slice(1), 10)
50
+ if (!isNaN(parsedPort)) {
51
+ return {host, port: parsedPort}
52
+ }
53
+ }
54
+ return {host}
55
+ }
56
+ }
57
+
58
+ // 检测裸 IPv6(多个冒号但无方括号):安全失败,当作 host-only
59
+ const colonCount = (s.match(/:/g) || []).length
60
+ if (colonCount >= 2) {
61
+ return {host: s}
62
+ }
63
+
64
+ // 普通格式: host:port 或 host
65
+ const colonIndex = s.lastIndexOf(':')
66
+ if (colonIndex !== -1) {
67
+ const host = s.slice(0, colonIndex)
68
+ const portStr = s.slice(colonIndex + 1)
69
+ const parsedPort = parseInt(portStr, 10)
70
+ if (!isNaN(parsedPort)) {
71
+ return {host, port: parsedPort}
72
+ }
73
+ }
74
+ return {host: s}
75
+ }
76
+
77
+ /**
78
+ * 解析 ProxyJump 字符串
79
+ * 支持格式:host, user@host, host:port, user@host:port, [ipv6]:port
80
+ * 注意:只解析第一跳,不支持逗号分隔的多跳链路
81
+ */
82
+ export function parseProxyJump(proxyJump: string): ParsedProxyJump | null {
83
+ if (!proxyJump) {
84
+ return null
85
+ }
86
+
87
+ // 取第一跳(如果有逗号分隔)
88
+ const firstJump = proxyJump.split(',')[0].trim()
89
+ if (!firstJump) {
90
+ return null
91
+ }
92
+
93
+ let user: string | undefined
94
+
95
+ // 解析 user@... 格式
96
+ const atIndex = firstJump.indexOf('@')
97
+ if (atIndex !== -1) {
98
+ user = firstJump.slice(0, atIndex)
99
+ const rest = firstJump.slice(atIndex + 1)
100
+ const {host, port} = parseHostPort(rest)
101
+ return {user, host, port}
102
+ }
103
+
104
+ const {host, port} = parseHostPort(firstJump)
105
+ return {user, host, port}
106
+ }
107
+
108
+ /**
109
+ * 展开 ~ 路径
110
+ */
111
+ function expandTilde(filePath: string): string {
112
+ if (filePath.startsWith('~')) {
113
+ return path.join(os.homedir(), filePath.slice(1))
114
+ }
115
+ return filePath
116
+ }
117
+
118
+ /**
119
+ * 剥离行尾注释
120
+ * "value # comment" -> "value"
121
+ */
122
+ function stripInlineComment(value: string): string {
123
+ // 查找不在引号内的 #
124
+ let inQuote = false
125
+ let quoteChar = ''
126
+ for (let i = 0; i < value.length; i++) {
127
+ const ch = value[i]
128
+ if (!inQuote && (ch === '"' || ch === '\'')) {
129
+ inQuote = true
130
+ quoteChar = ch
131
+ } else if (inQuote && ch === quoteChar) {
132
+ inQuote = false
133
+ } else if (!inQuote && ch === '#') {
134
+ return value.slice(0, i).trim()
135
+ }
136
+ }
137
+ return value.trim()
138
+ }
139
+
140
+ /**
141
+ * 解析 SSH config 文件
142
+ * 支持 Host 多别名和 Host * 继承
143
+ * 跳过 Match 块(避免条件配置被误应用)
144
+ */
145
+ export function parseSSHConfig(configPath?: string): SSHConfigHost[] {
146
+ const filePath = configPath || path.join(os.homedir(), '.ssh', 'config')
147
+
148
+ if (!fs.existsSync(filePath)) {
149
+ return []
150
+ }
151
+
152
+ const content = fs.readFileSync(filePath, 'utf-8')
153
+ const lines = content.split('\n')
154
+
155
+ // 第一遍:收集所有配置块
156
+ const blocks: ConfigBlock[] = []
157
+ let currentBlock: ConfigBlock | null = null
158
+ let globalDefaults: Omit<SSHConfigHost, 'host'> = {}
159
+ let inMatchBlock = false // 跳过 Match 块
160
+
161
+ for (const line of lines) {
162
+ const trimmed = line.trim()
163
+
164
+ // 跳过空行和注释
165
+ if (!trimmed || trimmed.startsWith('#')) {
166
+ continue
167
+ }
168
+
169
+ // 解析 key value(支持 = 和空格分隔)
170
+ const match = trimmed.match(/^(\S+)\s*[=\s]\s*(.+)$/)
171
+ if (!match) {
172
+ continue
173
+ }
174
+
175
+ const [, key, rawValue] = match
176
+ const keyLower = key.toLowerCase()
177
+ const value = stripInlineComment(rawValue)
178
+
179
+ if (keyLower === 'host') {
180
+ // Host 块开始,结束 Match 块
181
+ inMatchBlock = false
182
+
183
+ // 保存上一个 block
184
+ if (currentBlock) {
185
+ blocks.push(currentBlock)
186
+ }
187
+
188
+ // Host 行可能有多个别名/模式,用空格分隔
189
+ const patterns = value.split(/\s+/).filter(p => p.length > 0)
190
+ currentBlock = {patterns, config: {}}
191
+ } else if (keyLower === 'match') {
192
+ // Match 块开始,跳过直到下一个 Host
193
+ inMatchBlock = true
194
+ // 保存当前 block(如果有)
195
+ if (currentBlock) {
196
+ blocks.push(currentBlock)
197
+ currentBlock = null
198
+ }
199
+ } else if (!inMatchBlock && currentBlock) {
200
+ // 解析配置项(不在 Match 块内)
201
+ switch (keyLower) {
202
+ case 'hostname':
203
+ currentBlock.config.hostName = value
204
+ break
205
+ case 'user':
206
+ currentBlock.config.user = value
207
+ break
208
+ case 'port':
209
+ currentBlock.config.port = parseInt(value, 10)
210
+ break
211
+ case 'identityfile':
212
+ currentBlock.config.identityFile = expandTilde(value)
213
+ break
214
+ case 'proxyjump':
215
+ currentBlock.config.proxyJump = value
216
+ break
217
+ }
218
+ }
219
+ }
220
+
221
+ // 保存最后一个 block
222
+ if (currentBlock && !inMatchBlock) {
223
+ blocks.push(currentBlock)
224
+ }
225
+
226
+ // 第二遍:提取 Host * 的全局默认配置
227
+ for (const block of blocks) {
228
+ if (block.patterns.length === 1 && block.patterns[0] === '*') {
229
+ globalDefaults = {...block.config}
230
+ break
231
+ }
232
+ }
233
+
234
+ // 第三遍:展开所有 Host,应用继承
235
+ const hosts: SSHConfigHost[] = []
236
+
237
+ for (const block of blocks) {
238
+ for (const pattern of block.patterns) {
239
+ // 跳过通配符模式(*, *.example.com 等)
240
+ if (pattern.includes('*') || pattern.includes('?')) {
241
+ continue
242
+ }
243
+
244
+ // 合并配置:全局默认 + 当前块配置
245
+ const merged: SSHConfigHost = {
246
+ host: pattern,
247
+ ...globalDefaults,
248
+ ...block.config,
249
+ }
250
+
251
+ hosts.push(merged)
252
+ }
253
+ }
254
+
255
+ return hosts
256
+ }
257
+
258
+ /**
259
+ * 根据 Host 名称获取配置
260
+ */
261
+ export function getHostConfig(hostName: string, configPath?: string): SSHConfigHost | null {
262
+ const hosts = parseSSHConfig(configPath)
263
+ return hosts.find(h => h.host === hostName) || null
264
+ }
package/src/types.ts CHANGED
@@ -3,108 +3,108 @@
3
3
  */
4
4
 
5
5
  export interface SSHConnectionConfig {
6
- host: string;
7
- port: number;
8
- username: string;
9
- password?: string;
10
- privateKeyPath?: string;
11
- privateKey?: string;
12
- passphrase?: string;
13
- alias?: string;
14
- // 高级配置
15
- keepaliveInterval?: number; // 心跳间隔(毫秒)
16
- keepaliveCountMax?: number; // 最大心跳失败次数
17
- readyTimeout?: number; // 连接超时(毫秒)
18
- // 环境配置
19
- env?: Record<string, string>; // 环境变量
20
- lang?: string; // LANG 设置
21
- shell?: string; // Shell 类型
22
- // 跳板机
23
- jumpHost?: SSHConnectionConfig;
6
+ host: string;
7
+ port: number;
8
+ username: string;
9
+ password?: string;
10
+ privateKeyPath?: string;
11
+ privateKey?: string;
12
+ passphrase?: string;
13
+ alias?: string;
14
+ // 高级配置
15
+ keepaliveInterval?: number; // 心跳间隔(毫秒)
16
+ keepaliveCountMax?: number; // 最大心跳失败次数
17
+ readyTimeout?: number; // 连接超时(毫秒)
18
+ // 环境配置
19
+ env?: Record<string, string>; // 环境变量
20
+ lang?: string; // LANG 设置
21
+ shell?: string; // Shell 类型
22
+ // 跳板机
23
+ jumpHost?: SSHConnectionConfig;
24
24
  }
25
25
 
26
26
  export interface SSHSessionInfo {
27
- alias: string;
28
- host: string;
29
- port: number;
30
- username: string;
31
- connected: boolean;
32
- connectedAt: number;
33
- lastUsedAt: number;
34
- env?: Record<string, string>;
27
+ alias: string;
28
+ host: string;
29
+ port: number;
30
+ username: string;
31
+ connected: boolean;
32
+ connectedAt: number;
33
+ lastUsedAt: number;
34
+ env?: Record<string, string>;
35
35
  }
36
36
 
37
37
  export interface ExecOptions {
38
- timeout?: number; // 命令超时(毫秒)
39
- env?: Record<string, string>; // 额外环境变量
40
- cwd?: string; // 工作目录
41
- pty?: boolean; // 是否使用 PTY
42
- maxOutputSize?: number; // 最大输出大小(字节),默认 10MB
43
- // PTY 配置
44
- rows?: number;
45
- cols?: number;
46
- term?: string;
38
+ timeout?: number; // 命令超时(毫秒)
39
+ env?: Record<string, string>; // 额外环境变量
40
+ cwd?: string; // 工作目录
41
+ pty?: boolean; // 是否使用 PTY
42
+ maxOutputSize?: number; // 最大输出大小(字节),默认 10MB
43
+ // PTY 配置
44
+ rows?: number;
45
+ cols?: number;
46
+ term?: string;
47
47
  }
48
48
 
49
49
  export interface ExecResult {
50
- success: boolean;
51
- stdout: string;
52
- stderr: string;
53
- exitCode: number;
54
- duration: number; // 执行时间(毫秒)
50
+ success: boolean;
51
+ stdout: string;
52
+ stderr: string;
53
+ exitCode: number;
54
+ duration: number; // 执行时间(毫秒)
55
55
  }
56
56
 
57
57
  export interface FileInfo {
58
- name: string;
59
- path: string;
60
- size: number;
61
- isDirectory: boolean;
62
- isFile: boolean;
63
- isSymlink: boolean;
64
- permissions: string;
65
- owner: number;
66
- group: number;
67
- mtime: Date;
68
- atime: Date;
58
+ name: string;
59
+ path: string;
60
+ size: number;
61
+ isDirectory: boolean;
62
+ isFile: boolean;
63
+ isSymlink: boolean;
64
+ permissions: string;
65
+ owner: number;
66
+ group: number;
67
+ mtime: Date;
68
+ atime: Date;
69
69
  }
70
70
 
71
71
  export interface TransferProgress {
72
- transferred: number;
73
- total: number;
74
- percent: number;
72
+ transferred: number;
73
+ total: number;
74
+ percent: number;
75
75
  }
76
76
 
77
77
  // 持久化存储的会话配置(不含敏感信息)
78
78
  export interface PersistedSession {
79
- alias: string;
80
- host: string;
81
- port: number;
82
- username: string;
83
- connectedAt: number;
84
- env?: Record<string, string>;
79
+ alias: string;
80
+ host: string;
81
+ port: number;
82
+ username: string;
83
+ connectedAt: number;
84
+ env?: Record<string, string>;
85
85
  }
86
86
 
87
87
  // PTY 会话配置
88
88
  export interface PtyOptions {
89
- rows?: number;
90
- cols?: number;
91
- term?: string;
92
- env?: Record<string, string>;
93
- cwd?: string;
94
- bufferSize?: number; // 输出缓冲区大小,默认 1MB
89
+ rows?: number;
90
+ cols?: number;
91
+ term?: string;
92
+ env?: Record<string, string>;
93
+ cwd?: string;
94
+ bufferSize?: number; // 输出缓冲区大小,默认 1MB
95
95
  }
96
96
 
97
97
  // PTY 会话信息
98
98
  export interface PtySessionInfo {
99
- id: string;
100
- alias: string; // SSH 连接别名
101
- command: string; // 启动命令
102
- rows: number;
103
- cols: number;
104
- createdAt: number;
105
- lastReadAt: number;
106
- bufferSize: number; // 当前缓冲区大小
107
- active: boolean;
99
+ id: string;
100
+ alias: string; // SSH 连接别名
101
+ command: string; // 启动命令
102
+ rows: number;
103
+ cols: number;
104
+ createdAt: number;
105
+ lastReadAt: number;
106
+ bufferSize: number; // 当前缓冲区大小
107
+ active: boolean;
108
108
  }
109
109
 
110
110
  // 端口转发类型
@@ -112,22 +112,22 @@ export type ForwardType = 'local' | 'remote';
112
112
 
113
113
  // 端口转发配置
114
114
  export interface PortForwardConfig {
115
- type: ForwardType;
116
- localHost: string;
117
- localPort: number;
118
- remoteHost: string;
119
- remotePort: number;
115
+ type: ForwardType;
116
+ localHost: string;
117
+ localPort: number;
118
+ remoteHost: string;
119
+ remotePort: number;
120
120
  }
121
121
 
122
122
  // 端口转发信息
123
123
  export interface PortForwardInfo {
124
- id: string;
125
- alias: string; // SSH 连接别名
126
- type: ForwardType;
127
- localHost: string;
128
- localPort: number;
129
- remoteHost: string;
130
- remotePort: number;
131
- createdAt: number;
132
- active: boolean;
124
+ id: string;
125
+ alias: string; // SSH 连接别名
126
+ type: ForwardType;
127
+ localHost: string;
128
+ localPort: number;
129
+ remoteHost: string;
130
+ remotePort: number;
131
+ createdAt: number;
132
+ active: boolean;
133
133
  }
package/tsconfig.json CHANGED
@@ -11,6 +11,11 @@
11
11
  "forceConsistentCasingInFileNames": true,
12
12
  "declaration": true
13
13
  },
14
- "include": ["src/**/*"],
15
- "exclude": ["node_modules", "dist"]
14
+ "include": [
15
+ "src/**/*"
16
+ ],
17
+ "exclude": [
18
+ "node_modules",
19
+ "dist"
20
+ ]
16
21
  }