@pyrokine/mcp-ssh 1.0.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/LICENSE +21 -0
- package/README.md +353 -0
- package/README_zh.md +353 -0
- package/dist/file-ops.d.ts +78 -0
- package/dist/file-ops.js +572 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +804 -0
- package/dist/session-manager.d.ts +193 -0
- package/dist/session-manager.js +792 -0
- package/dist/types.d.ts +111 -0
- package/dist/types.js +4 -0
- package/package.json +43 -0
- package/src/file-ops.ts +730 -0
- package/src/index.ts +938 -0
- package/src/session-manager.ts +982 -0
- package/src/types.ts +133 -0
- package/tsconfig.json +16 -0
package/dist/file-ops.js
ADDED
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSH File Operations - 文件操作
|
|
3
|
+
*/
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import { execSync } from 'child_process';
|
|
7
|
+
import { sessionManager } from './session-manager.js';
|
|
8
|
+
/**
|
|
9
|
+
* 上传文件
|
|
10
|
+
*/
|
|
11
|
+
export async function uploadFile(alias, localPath, remotePath, onProgress) {
|
|
12
|
+
if (!fs.existsSync(localPath)) {
|
|
13
|
+
throw new Error(`Local file not found: ${localPath}`);
|
|
14
|
+
}
|
|
15
|
+
const sftp = await sessionManager.getSftp(alias);
|
|
16
|
+
const stats = fs.statSync(localPath);
|
|
17
|
+
const totalSize = stats.size;
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
const readStream = fs.createReadStream(localPath);
|
|
20
|
+
const writeStream = sftp.createWriteStream(remotePath);
|
|
21
|
+
let settled = false;
|
|
22
|
+
const cleanup = (err) => {
|
|
23
|
+
if (settled)
|
|
24
|
+
return;
|
|
25
|
+
settled = true;
|
|
26
|
+
sftp.end();
|
|
27
|
+
if (err)
|
|
28
|
+
reject(err);
|
|
29
|
+
};
|
|
30
|
+
let transferred = 0;
|
|
31
|
+
readStream.on('data', (chunk) => {
|
|
32
|
+
transferred += chunk.length;
|
|
33
|
+
if (onProgress) {
|
|
34
|
+
onProgress({
|
|
35
|
+
transferred,
|
|
36
|
+
total: totalSize,
|
|
37
|
+
percent: totalSize > 0 ? Math.round((transferred / totalSize) * 100) : 100,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
readStream.on('error', (err) => cleanup(err));
|
|
42
|
+
writeStream.on('error', (err) => cleanup(err));
|
|
43
|
+
writeStream.on('close', () => {
|
|
44
|
+
if (!settled) {
|
|
45
|
+
settled = true;
|
|
46
|
+
sftp.end();
|
|
47
|
+
resolve({ success: true, size: totalSize });
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
readStream.pipe(writeStream);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* 下载文件
|
|
55
|
+
*/
|
|
56
|
+
export async function downloadFile(alias, remotePath, localPath, onProgress) {
|
|
57
|
+
const sftp = await sessionManager.getSftp(alias);
|
|
58
|
+
// 获取远程文件大小
|
|
59
|
+
const stats = await new Promise((resolve, reject) => {
|
|
60
|
+
sftp.stat(remotePath, (err, stats) => {
|
|
61
|
+
if (err)
|
|
62
|
+
reject(err);
|
|
63
|
+
else
|
|
64
|
+
resolve(stats);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
const totalSize = stats.size;
|
|
68
|
+
// 确保本地目录存在
|
|
69
|
+
const localDir = path.dirname(localPath);
|
|
70
|
+
if (!fs.existsSync(localDir)) {
|
|
71
|
+
fs.mkdirSync(localDir, { recursive: true });
|
|
72
|
+
}
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
const readStream = sftp.createReadStream(remotePath);
|
|
75
|
+
const writeStream = fs.createWriteStream(localPath);
|
|
76
|
+
let settled = false;
|
|
77
|
+
const cleanup = (err) => {
|
|
78
|
+
if (settled)
|
|
79
|
+
return;
|
|
80
|
+
settled = true;
|
|
81
|
+
sftp.end();
|
|
82
|
+
if (err)
|
|
83
|
+
reject(err);
|
|
84
|
+
};
|
|
85
|
+
let transferred = 0;
|
|
86
|
+
readStream.on('data', (chunk) => {
|
|
87
|
+
transferred += chunk.length;
|
|
88
|
+
if (onProgress) {
|
|
89
|
+
onProgress({
|
|
90
|
+
transferred,
|
|
91
|
+
total: totalSize,
|
|
92
|
+
percent: totalSize > 0 ? Math.round((transferred / totalSize) * 100) : 100,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
readStream.on('error', (err) => cleanup(err));
|
|
97
|
+
writeStream.on('error', (err) => cleanup(err));
|
|
98
|
+
writeStream.on('close', () => {
|
|
99
|
+
if (!settled) {
|
|
100
|
+
settled = true;
|
|
101
|
+
sftp.end();
|
|
102
|
+
resolve({ success: true, size: totalSize });
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
readStream.pipe(writeStream);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* 读取远程文件内容
|
|
110
|
+
*/
|
|
111
|
+
export async function readFile(alias, remotePath, maxBytes = 1024 * 1024 // 默认最大 1MB
|
|
112
|
+
) {
|
|
113
|
+
const sftp = await sessionManager.getSftp(alias);
|
|
114
|
+
// 获取文件大小
|
|
115
|
+
const stats = await new Promise((resolve, reject) => {
|
|
116
|
+
sftp.stat(remotePath, (err, stats) => {
|
|
117
|
+
if (err)
|
|
118
|
+
reject(err);
|
|
119
|
+
else
|
|
120
|
+
resolve(stats);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
const actualSize = stats.size;
|
|
124
|
+
const truncated = actualSize > maxBytes;
|
|
125
|
+
// 处理空文件
|
|
126
|
+
if (actualSize === 0) {
|
|
127
|
+
sftp.end();
|
|
128
|
+
return { content: '', size: 0, truncated: false };
|
|
129
|
+
}
|
|
130
|
+
const readSize = Math.min(actualSize, maxBytes);
|
|
131
|
+
return new Promise((resolve, reject) => {
|
|
132
|
+
const chunks = [];
|
|
133
|
+
const readStream = sftp.createReadStream(remotePath, {
|
|
134
|
+
start: 0,
|
|
135
|
+
end: readSize - 1,
|
|
136
|
+
});
|
|
137
|
+
readStream.on('data', (chunk) => {
|
|
138
|
+
chunks.push(chunk);
|
|
139
|
+
});
|
|
140
|
+
readStream.on('end', () => {
|
|
141
|
+
sftp.end();
|
|
142
|
+
const content = Buffer.concat(chunks).toString('utf-8');
|
|
143
|
+
resolve({
|
|
144
|
+
content,
|
|
145
|
+
size: actualSize,
|
|
146
|
+
truncated,
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
readStream.on('error', (err) => {
|
|
150
|
+
sftp.end();
|
|
151
|
+
reject(err);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* 写入远程文件
|
|
157
|
+
*/
|
|
158
|
+
export async function writeFile(alias, remotePath, content, append = false) {
|
|
159
|
+
const sftp = await sessionManager.getSftp(alias);
|
|
160
|
+
const flags = append ? 'a' : 'w';
|
|
161
|
+
return new Promise((resolve, reject) => {
|
|
162
|
+
const writeStream = sftp.createWriteStream(remotePath, { flags });
|
|
163
|
+
writeStream.on('close', () => {
|
|
164
|
+
sftp.end();
|
|
165
|
+
resolve({ success: true, size: content.length });
|
|
166
|
+
});
|
|
167
|
+
writeStream.on('error', (err) => {
|
|
168
|
+
sftp.end();
|
|
169
|
+
reject(err);
|
|
170
|
+
});
|
|
171
|
+
writeStream.write(content);
|
|
172
|
+
writeStream.end();
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* 列出目录内容
|
|
177
|
+
*/
|
|
178
|
+
export async function listDir(alias, remotePath, showHidden = false) {
|
|
179
|
+
const sftp = await sessionManager.getSftp(alias);
|
|
180
|
+
return new Promise((resolve, reject) => {
|
|
181
|
+
sftp.readdir(remotePath, (err, list) => {
|
|
182
|
+
if (err) {
|
|
183
|
+
sftp.end();
|
|
184
|
+
reject(err);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const files = list
|
|
188
|
+
.filter((item) => showHidden || !item.filename.startsWith('.'))
|
|
189
|
+
.map((item) => ({
|
|
190
|
+
name: item.filename,
|
|
191
|
+
path: path.posix.join(remotePath, item.filename),
|
|
192
|
+
size: item.attrs.size,
|
|
193
|
+
isDirectory: (item.attrs.mode & 0o40000) !== 0,
|
|
194
|
+
isFile: (item.attrs.mode & 0o100000) !== 0,
|
|
195
|
+
isSymlink: (item.attrs.mode & 0o120000) !== 0,
|
|
196
|
+
permissions: formatPermissions(item.attrs.mode),
|
|
197
|
+
owner: item.attrs.uid,
|
|
198
|
+
group: item.attrs.gid,
|
|
199
|
+
mtime: new Date(item.attrs.mtime * 1000),
|
|
200
|
+
atime: new Date(item.attrs.atime * 1000),
|
|
201
|
+
}))
|
|
202
|
+
.sort((a, b) => {
|
|
203
|
+
// 目录在前
|
|
204
|
+
if (a.isDirectory !== b.isDirectory) {
|
|
205
|
+
return a.isDirectory ? -1 : 1;
|
|
206
|
+
}
|
|
207
|
+
return a.name.localeCompare(b.name);
|
|
208
|
+
});
|
|
209
|
+
sftp.end();
|
|
210
|
+
resolve(files);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* 获取文件信息
|
|
216
|
+
*/
|
|
217
|
+
export async function getFileInfo(alias, remotePath) {
|
|
218
|
+
const sftp = await sessionManager.getSftp(alias);
|
|
219
|
+
return new Promise((resolve, reject) => {
|
|
220
|
+
sftp.stat(remotePath, (err, stats) => {
|
|
221
|
+
sftp.end();
|
|
222
|
+
if (err) {
|
|
223
|
+
reject(err);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
resolve({
|
|
227
|
+
name: path.posix.basename(remotePath),
|
|
228
|
+
path: remotePath,
|
|
229
|
+
size: stats.size,
|
|
230
|
+
isDirectory: (stats.mode & 0o40000) !== 0,
|
|
231
|
+
isFile: (stats.mode & 0o100000) !== 0,
|
|
232
|
+
isSymlink: (stats.mode & 0o120000) !== 0,
|
|
233
|
+
permissions: formatPermissions(stats.mode),
|
|
234
|
+
owner: stats.uid,
|
|
235
|
+
group: stats.gid,
|
|
236
|
+
mtime: new Date(stats.mtime * 1000),
|
|
237
|
+
atime: new Date(stats.atime * 1000),
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* 检查文件是否存在
|
|
244
|
+
*/
|
|
245
|
+
export async function fileExists(alias, remotePath) {
|
|
246
|
+
const sftp = await sessionManager.getSftp(alias);
|
|
247
|
+
return new Promise((resolve) => {
|
|
248
|
+
sftp.stat(remotePath, (err) => {
|
|
249
|
+
sftp.end();
|
|
250
|
+
resolve(!err);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* 创建目录
|
|
256
|
+
*/
|
|
257
|
+
export async function mkdir(alias, remotePath, recursive = false) {
|
|
258
|
+
if (recursive) {
|
|
259
|
+
// 通过 exec 实现递归创建
|
|
260
|
+
const result = await sessionManager.exec(alias, `mkdir -p "${remotePath}"`);
|
|
261
|
+
return result.exitCode === 0;
|
|
262
|
+
}
|
|
263
|
+
const sftp = await sessionManager.getSftp(alias);
|
|
264
|
+
return new Promise((resolve, reject) => {
|
|
265
|
+
sftp.mkdir(remotePath, (err) => {
|
|
266
|
+
sftp.end();
|
|
267
|
+
if (err)
|
|
268
|
+
reject(err);
|
|
269
|
+
else
|
|
270
|
+
resolve(true);
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* 删除文件
|
|
276
|
+
*/
|
|
277
|
+
export async function removeFile(alias, remotePath) {
|
|
278
|
+
const sftp = await sessionManager.getSftp(alias);
|
|
279
|
+
return new Promise((resolve, reject) => {
|
|
280
|
+
sftp.unlink(remotePath, (err) => {
|
|
281
|
+
sftp.end();
|
|
282
|
+
if (err)
|
|
283
|
+
reject(err);
|
|
284
|
+
else
|
|
285
|
+
resolve(true);
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* 检查远程是否安装 rsync
|
|
291
|
+
*/
|
|
292
|
+
export async function checkRsync(alias) {
|
|
293
|
+
try {
|
|
294
|
+
const result = await sessionManager.exec(alias, 'which rsync');
|
|
295
|
+
return result.exitCode === 0 && result.stdout.trim().length > 0;
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* 智能文件同步(优先使用 rsync)
|
|
303
|
+
*
|
|
304
|
+
* @param alias SSH 连接别名
|
|
305
|
+
* @param localPath 本地路径
|
|
306
|
+
* @param remotePath 远程路径
|
|
307
|
+
* @param direction 同步方向:'upload' 或 'download'
|
|
308
|
+
* @param options 同步选项
|
|
309
|
+
*/
|
|
310
|
+
export async function syncFiles(alias, localPath, remotePath, direction, options = {}) {
|
|
311
|
+
// 检查远程 rsync
|
|
312
|
+
const hasRsync = await checkRsync(alias);
|
|
313
|
+
if (hasRsync) {
|
|
314
|
+
// 使用 rsync(通过远程端执行)
|
|
315
|
+
return syncWithRsync(alias, localPath, remotePath, direction, options);
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
// 回退到 SFTP
|
|
319
|
+
return syncWithSftp(alias, localPath, remotePath, direction, options);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* 转义 shell 路径参数
|
|
324
|
+
*/
|
|
325
|
+
function escapeShellPath(p) {
|
|
326
|
+
return `'${p.replace(/'/g, "'\\''")}'`;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* 使用 rsync 同步文件
|
|
330
|
+
* 通过本地执行 rsync 连接到远程(需要密钥认证或 ssh-agent)
|
|
331
|
+
*/
|
|
332
|
+
async function syncWithRsync(alias, localPath, remotePath, direction, options) {
|
|
333
|
+
// 检查本地是否有 rsync
|
|
334
|
+
let hasLocalRsync = false;
|
|
335
|
+
try {
|
|
336
|
+
execSync('which rsync', { stdio: 'pipe' });
|
|
337
|
+
hasLocalRsync = true;
|
|
338
|
+
}
|
|
339
|
+
catch { }
|
|
340
|
+
if (!hasLocalRsync) {
|
|
341
|
+
// 本地没有 rsync,回退到 SFTP
|
|
342
|
+
return syncWithSftp(alias, localPath, remotePath, direction, options);
|
|
343
|
+
}
|
|
344
|
+
// 获取会话信息以构建 rsync 命令
|
|
345
|
+
const sessions = sessionManager.listSessions();
|
|
346
|
+
const sessionInfo = sessions.find(s => s.alias === alias);
|
|
347
|
+
if (!sessionInfo) {
|
|
348
|
+
throw new Error(`Session '${alias}' not found`);
|
|
349
|
+
}
|
|
350
|
+
// 构建 rsync 参数
|
|
351
|
+
const args = ['-avz', '--progress'];
|
|
352
|
+
if (options.delete) {
|
|
353
|
+
args.push('--delete');
|
|
354
|
+
}
|
|
355
|
+
if (options.dryRun) {
|
|
356
|
+
args.push('--dry-run');
|
|
357
|
+
}
|
|
358
|
+
if (options.recursive === false) {
|
|
359
|
+
args.push('--dirs'); // 不递归,只传输目录本身
|
|
360
|
+
}
|
|
361
|
+
if (options.exclude) {
|
|
362
|
+
for (const pattern of options.exclude) {
|
|
363
|
+
args.push(`--exclude=${escapeShellPath(pattern)}`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
// 构建 rsync 命令(本地执行)
|
|
367
|
+
// 注意:这需要密钥认证或 ssh-agent,密码认证不支持
|
|
368
|
+
const sshCmd = `ssh -p ${sessionInfo.port} -o StrictHostKeyChecking=no -o BatchMode=yes`;
|
|
369
|
+
const remoteSpec = `${sessionInfo.username}@${sessionInfo.host}:${escapeShellPath(remotePath)}`;
|
|
370
|
+
const rsyncCmd = direction === 'upload'
|
|
371
|
+
? `rsync ${args.join(' ')} -e "${sshCmd}" ${escapeShellPath(localPath)} ${remoteSpec}`
|
|
372
|
+
: `rsync ${args.join(' ')} -e "${sshCmd}" ${remoteSpec} ${escapeShellPath(localPath)}`;
|
|
373
|
+
try {
|
|
374
|
+
const result = execSync(rsyncCmd, {
|
|
375
|
+
encoding: 'utf-8',
|
|
376
|
+
timeout: 600000, // 10 分钟超时
|
|
377
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
378
|
+
});
|
|
379
|
+
// 解析 rsync 输出统计文件数
|
|
380
|
+
const lines = result.split('\n');
|
|
381
|
+
let filesTransferred = 0;
|
|
382
|
+
for (const line of lines) {
|
|
383
|
+
if (line.trim() && !line.startsWith('sending') && !line.startsWith('receiving') && !line.startsWith('total')) {
|
|
384
|
+
filesTransferred++;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return {
|
|
388
|
+
success: true,
|
|
389
|
+
method: 'rsync',
|
|
390
|
+
filesTransferred,
|
|
391
|
+
output: result,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
catch {
|
|
395
|
+
// rsync 失败(可能是密码认证),回退到 SFTP
|
|
396
|
+
return syncWithSftp(alias, localPath, remotePath, direction, options);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* 使用 SFTP 同步文件
|
|
401
|
+
*/
|
|
402
|
+
async function syncWithSftp(alias, localPath, remotePath, direction, options) {
|
|
403
|
+
// SFTP 模式不支持 delete 选项
|
|
404
|
+
const warnings = [];
|
|
405
|
+
if (options.delete) {
|
|
406
|
+
warnings.push('delete option is not supported in SFTP mode (requires rsync)');
|
|
407
|
+
}
|
|
408
|
+
if (options.dryRun) {
|
|
409
|
+
return {
|
|
410
|
+
success: true,
|
|
411
|
+
method: 'sftp',
|
|
412
|
+
output: 'Dry run mode: would transfer files via SFTP' + (warnings.length ? `. Warning: ${warnings.join('; ')}` : ''),
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
try {
|
|
416
|
+
let result;
|
|
417
|
+
if (direction === 'upload') {
|
|
418
|
+
// 检查是否是目录
|
|
419
|
+
const stats = fs.statSync(localPath);
|
|
420
|
+
if (stats.isDirectory() && options.recursive !== false) {
|
|
421
|
+
result = await uploadDirectory(alias, localPath, remotePath, options.exclude);
|
|
422
|
+
return {
|
|
423
|
+
success: true,
|
|
424
|
+
method: 'sftp',
|
|
425
|
+
filesTransferred: result.fileCount,
|
|
426
|
+
bytesTransferred: result.totalSize,
|
|
427
|
+
output: warnings.length ? `Warning: ${warnings.join('; ')}` : undefined,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
result = await uploadFile(alias, localPath, remotePath);
|
|
432
|
+
return {
|
|
433
|
+
success: result.success,
|
|
434
|
+
method: 'sftp',
|
|
435
|
+
filesTransferred: 1,
|
|
436
|
+
bytesTransferred: result.size,
|
|
437
|
+
output: warnings.length ? `Warning: ${warnings.join('; ')}` : undefined,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
// 下载
|
|
443
|
+
const info = await getFileInfo(alias, remotePath);
|
|
444
|
+
if (info.isDirectory && options.recursive !== false) {
|
|
445
|
+
result = await downloadDirectory(alias, remotePath, localPath, options.exclude);
|
|
446
|
+
return {
|
|
447
|
+
success: true,
|
|
448
|
+
method: 'sftp',
|
|
449
|
+
filesTransferred: result.fileCount,
|
|
450
|
+
bytesTransferred: result.totalSize,
|
|
451
|
+
output: warnings.length ? `Warning: ${warnings.join('; ')}` : undefined,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
result = await downloadFile(alias, remotePath, localPath);
|
|
456
|
+
return {
|
|
457
|
+
success: result.success,
|
|
458
|
+
method: 'sftp',
|
|
459
|
+
filesTransferred: 1,
|
|
460
|
+
bytesTransferred: result.size,
|
|
461
|
+
output: warnings.length ? `Warning: ${warnings.join('; ')}` : undefined,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
catch (err) {
|
|
467
|
+
return {
|
|
468
|
+
success: false,
|
|
469
|
+
method: 'sftp',
|
|
470
|
+
output: err.message,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* 递归上传目录
|
|
476
|
+
*/
|
|
477
|
+
async function uploadDirectory(alias, localPath, remotePath, exclude) {
|
|
478
|
+
let fileCount = 0;
|
|
479
|
+
let totalSize = 0;
|
|
480
|
+
// 确保远程目录存在
|
|
481
|
+
await mkdir(alias, remotePath, true);
|
|
482
|
+
const items = fs.readdirSync(localPath);
|
|
483
|
+
for (const item of items) {
|
|
484
|
+
// 检查排除模式
|
|
485
|
+
if (exclude && exclude.some(pattern => matchPattern(item, pattern))) {
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
const itemLocalPath = path.join(localPath, item);
|
|
489
|
+
const itemRemotePath = path.posix.join(remotePath, item);
|
|
490
|
+
const stats = fs.statSync(itemLocalPath);
|
|
491
|
+
if (stats.isDirectory()) {
|
|
492
|
+
const result = await uploadDirectory(alias, itemLocalPath, itemRemotePath, exclude);
|
|
493
|
+
fileCount += result.fileCount;
|
|
494
|
+
totalSize += result.totalSize;
|
|
495
|
+
}
|
|
496
|
+
else if (stats.isFile()) {
|
|
497
|
+
await uploadFile(alias, itemLocalPath, itemRemotePath);
|
|
498
|
+
fileCount++;
|
|
499
|
+
totalSize += stats.size;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return { fileCount, totalSize };
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* 递归下载目录
|
|
506
|
+
*/
|
|
507
|
+
async function downloadDirectory(alias, remotePath, localPath, exclude) {
|
|
508
|
+
let fileCount = 0;
|
|
509
|
+
let totalSize = 0;
|
|
510
|
+
// 确保本地目录存在
|
|
511
|
+
if (!fs.existsSync(localPath)) {
|
|
512
|
+
fs.mkdirSync(localPath, { recursive: true });
|
|
513
|
+
}
|
|
514
|
+
const items = await listDir(alias, remotePath, true);
|
|
515
|
+
for (const item of items) {
|
|
516
|
+
// 检查排除模式
|
|
517
|
+
if (exclude && exclude.some(pattern => matchPattern(item.name, pattern))) {
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
const itemLocalPath = path.join(localPath, item.name);
|
|
521
|
+
if (item.isDirectory) {
|
|
522
|
+
const result = await downloadDirectory(alias, item.path, itemLocalPath, exclude);
|
|
523
|
+
fileCount += result.fileCount;
|
|
524
|
+
totalSize += result.totalSize;
|
|
525
|
+
}
|
|
526
|
+
else if (item.isFile) {
|
|
527
|
+
await downloadFile(alias, item.path, itemLocalPath);
|
|
528
|
+
fileCount++;
|
|
529
|
+
totalSize += item.size;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return { fileCount, totalSize };
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* 简单的模式匹配(支持 * 和 ?)
|
|
536
|
+
*/
|
|
537
|
+
function matchPattern(name, pattern) {
|
|
538
|
+
const regexPattern = pattern
|
|
539
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // 转义特殊字符
|
|
540
|
+
.replace(/\*/g, '.*')
|
|
541
|
+
.replace(/\?/g, '.');
|
|
542
|
+
return new RegExp(`^${regexPattern}$`).test(name);
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* 格式化权限字符串
|
|
546
|
+
*/
|
|
547
|
+
function formatPermissions(mode) {
|
|
548
|
+
const types = {
|
|
549
|
+
0o40000: 'd',
|
|
550
|
+
0o120000: 'l',
|
|
551
|
+
0o100000: '-',
|
|
552
|
+
};
|
|
553
|
+
let type = '-';
|
|
554
|
+
for (const [mask, char] of Object.entries(types)) {
|
|
555
|
+
if ((mode & parseInt(mask)) !== 0) {
|
|
556
|
+
type = char;
|
|
557
|
+
break;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
const perms = [
|
|
561
|
+
(mode & 0o400) ? 'r' : '-',
|
|
562
|
+
(mode & 0o200) ? 'w' : '-',
|
|
563
|
+
(mode & 0o100) ? 'x' : '-',
|
|
564
|
+
(mode & 0o040) ? 'r' : '-',
|
|
565
|
+
(mode & 0o020) ? 'w' : '-',
|
|
566
|
+
(mode & 0o010) ? 'x' : '-',
|
|
567
|
+
(mode & 0o004) ? 'r' : '-',
|
|
568
|
+
(mode & 0o002) ? 'w' : '-',
|
|
569
|
+
(mode & 0o001) ? 'x' : '-',
|
|
570
|
+
];
|
|
571
|
+
return type + perms.join('');
|
|
572
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* SSH MCP Pro - Main Server Entry
|
|
4
|
+
*
|
|
5
|
+
* A comprehensive SSH MCP Server for Claude Code
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Multiple authentication methods (password, key, agent)
|
|
9
|
+
* - Connection pooling with keepalive
|
|
10
|
+
* - Session persistence
|
|
11
|
+
* - Command execution (exec, sudo, su)
|
|
12
|
+
* - File operations (upload, download, read, write)
|
|
13
|
+
* - Environment configuration
|
|
14
|
+
* - Jump host support
|
|
15
|
+
*/
|
|
16
|
+
export {};
|