@yivan-lab/pretty-please 1.1.0 → 1.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.
Files changed (52) hide show
  1. package/README.md +283 -1
  2. package/bin/pls.tsx +1022 -104
  3. package/dist/bin/pls.js +894 -84
  4. package/dist/package.json +4 -4
  5. package/dist/src/alias.d.ts +41 -0
  6. package/dist/src/alias.js +240 -0
  7. package/dist/src/chat-history.js +10 -1
  8. package/dist/src/components/Chat.js +2 -1
  9. package/dist/src/components/CodeColorizer.js +26 -20
  10. package/dist/src/components/CommandBox.js +2 -1
  11. package/dist/src/components/ConfirmationPrompt.js +2 -1
  12. package/dist/src/components/Duration.js +2 -1
  13. package/dist/src/components/InlineRenderer.js +2 -1
  14. package/dist/src/components/MarkdownDisplay.js +2 -1
  15. package/dist/src/components/MultiStepCommandGenerator.d.ts +3 -1
  16. package/dist/src/components/MultiStepCommandGenerator.js +20 -10
  17. package/dist/src/components/TableRenderer.js +2 -1
  18. package/dist/src/config.d.ts +34 -3
  19. package/dist/src/config.js +71 -31
  20. package/dist/src/multi-step.d.ts +22 -6
  21. package/dist/src/multi-step.js +27 -4
  22. package/dist/src/remote-history.d.ts +63 -0
  23. package/dist/src/remote-history.js +315 -0
  24. package/dist/src/remote.d.ts +113 -0
  25. package/dist/src/remote.js +634 -0
  26. package/dist/src/shell-hook.d.ts +53 -0
  27. package/dist/src/shell-hook.js +242 -19
  28. package/dist/src/ui/theme.d.ts +27 -24
  29. package/dist/src/ui/theme.js +71 -21
  30. package/dist/src/upgrade.d.ts +41 -0
  31. package/dist/src/upgrade.js +348 -0
  32. package/dist/src/utils/console.js +22 -11
  33. package/package.json +4 -4
  34. package/src/alias.ts +301 -0
  35. package/src/chat-history.ts +11 -1
  36. package/src/components/Chat.tsx +2 -1
  37. package/src/components/CodeColorizer.tsx +27 -19
  38. package/src/components/CommandBox.tsx +2 -1
  39. package/src/components/ConfirmationPrompt.tsx +2 -1
  40. package/src/components/Duration.tsx +2 -1
  41. package/src/components/InlineRenderer.tsx +2 -1
  42. package/src/components/MarkdownDisplay.tsx +2 -1
  43. package/src/components/MultiStepCommandGenerator.tsx +25 -11
  44. package/src/components/TableRenderer.tsx +2 -1
  45. package/src/config.ts +117 -32
  46. package/src/multi-step.ts +43 -6
  47. package/src/remote-history.ts +390 -0
  48. package/src/remote.ts +800 -0
  49. package/src/shell-hook.ts +271 -19
  50. package/src/ui/theme.ts +101 -24
  51. package/src/upgrade.ts +397 -0
  52. package/src/utils/console.ts +22 -11
@@ -0,0 +1,634 @@
1
+ /**
2
+ * 远程执行器模块
3
+ * 通过 SSH 在远程服务器上执行命令
4
+ */
5
+ import { spawn } from 'child_process';
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import os from 'os';
9
+ import readline from 'readline';
10
+ import chalk from 'chalk';
11
+ import { CONFIG_DIR, getConfig, saveConfig } from './config.js';
12
+ import { getCurrentTheme } from './ui/theme.js';
13
+ // 获取主题颜色
14
+ function getColors() {
15
+ const theme = getCurrentTheme();
16
+ return {
17
+ primary: theme.primary,
18
+ secondary: theme.secondary,
19
+ success: theme.success,
20
+ error: theme.error,
21
+ warning: theme.warning,
22
+ muted: theme.text.muted,
23
+ };
24
+ }
25
+ // 远程服务器数据目录
26
+ const REMOTES_DIR = path.join(CONFIG_DIR, 'remotes');
27
+ // SSH ControlMaster 配置
28
+ const SSH_CONTROL_PERSIST = '10m'; // 连接保持 10 分钟
29
+ /**
30
+ * 确保远程服务器数据目录存在
31
+ */
32
+ function ensureRemotesDir() {
33
+ if (!fs.existsSync(REMOTES_DIR)) {
34
+ fs.mkdirSync(REMOTES_DIR, { recursive: true });
35
+ }
36
+ }
37
+ /**
38
+ * 获取远程服务器数据目录
39
+ */
40
+ function getRemoteDataDir(name) {
41
+ return path.join(REMOTES_DIR, name);
42
+ }
43
+ /**
44
+ * 获取 SSH ControlMaster socket 路径
45
+ */
46
+ function getSSHSocketPath(name) {
47
+ return path.join(REMOTES_DIR, name, 'ssh.sock');
48
+ }
49
+ /**
50
+ * 检查 ControlMaster 连接是否存在
51
+ */
52
+ function isControlMasterActive(name) {
53
+ const socketPath = getSSHSocketPath(name);
54
+ return fs.existsSync(socketPath);
55
+ }
56
+ /**
57
+ * 关闭 ControlMaster 连接
58
+ */
59
+ export async function closeControlMaster(name) {
60
+ const remote = getRemote(name);
61
+ if (!remote)
62
+ return;
63
+ const socketPath = getSSHSocketPath(name);
64
+ if (!fs.existsSync(socketPath))
65
+ return;
66
+ // 使用 ssh -O exit 关闭 master 连接
67
+ const args = ['-O', 'exit', '-o', `ControlPath=${socketPath}`, `${remote.user}@${remote.host}`];
68
+ return new Promise((resolve) => {
69
+ const child = spawn('ssh', args, { stdio: 'ignore' });
70
+ child.on('close', () => {
71
+ // 确保 socket 文件被删除
72
+ if (fs.existsSync(socketPath)) {
73
+ try {
74
+ fs.unlinkSync(socketPath);
75
+ }
76
+ catch {
77
+ // 忽略错误
78
+ }
79
+ }
80
+ resolve();
81
+ });
82
+ child.on('error', () => resolve());
83
+ });
84
+ }
85
+ /**
86
+ * 确保远程服务器数据目录存在
87
+ */
88
+ function ensureRemoteDataDir(name) {
89
+ const dir = getRemoteDataDir(name);
90
+ if (!fs.existsSync(dir)) {
91
+ fs.mkdirSync(dir, { recursive: true });
92
+ }
93
+ }
94
+ // ================== 远程服务器管理 ==================
95
+ /**
96
+ * 获取所有远程服务器配置
97
+ */
98
+ export function getRemotes() {
99
+ const config = getConfig();
100
+ return config.remotes || {};
101
+ }
102
+ /**
103
+ * 获取单个远程服务器配置
104
+ */
105
+ export function getRemote(name) {
106
+ const remotes = getRemotes();
107
+ return remotes[name] || null;
108
+ }
109
+ /**
110
+ * 解析 user@host:port 格式
111
+ */
112
+ function parseHostString(hostStr) {
113
+ let user = '';
114
+ let host = hostStr;
115
+ let port = 22;
116
+ // 解析 user@host
117
+ if (hostStr.includes('@')) {
118
+ const atIndex = hostStr.indexOf('@');
119
+ user = hostStr.substring(0, atIndex);
120
+ host = hostStr.substring(atIndex + 1);
121
+ }
122
+ // 解析 host:port
123
+ if (host.includes(':')) {
124
+ const colonIndex = host.lastIndexOf(':');
125
+ const portStr = host.substring(colonIndex + 1);
126
+ const parsedPort = parseInt(portStr, 10);
127
+ if (!isNaN(parsedPort)) {
128
+ port = parsedPort;
129
+ host = host.substring(0, colonIndex);
130
+ }
131
+ }
132
+ return { user, host, port };
133
+ }
134
+ /**
135
+ * 添加远程服务器
136
+ */
137
+ export function addRemote(name, hostStr, options = {}) {
138
+ // 验证名称
139
+ if (!name || !name.trim()) {
140
+ throw new Error('服务器名称不能为空');
141
+ }
142
+ if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
143
+ throw new Error('服务器名称只能包含字母、数字、下划线和连字符');
144
+ }
145
+ // 解析 host 字符串
146
+ const { user, host, port } = parseHostString(hostStr);
147
+ if (!host) {
148
+ throw new Error('主机地址不能为空');
149
+ }
150
+ if (!user) {
151
+ throw new Error('用户名不能为空,请使用 user@host 格式');
152
+ }
153
+ // 验证密钥文件
154
+ if (options.key) {
155
+ const keyPath = options.key.replace(/^~/, os.homedir());
156
+ if (!fs.existsSync(keyPath)) {
157
+ throw new Error(`密钥文件不存在: ${options.key}`);
158
+ }
159
+ }
160
+ const config = getConfig();
161
+ if (!config.remotes) {
162
+ config.remotes = {};
163
+ }
164
+ // 检查是否已存在
165
+ if (config.remotes[name]) {
166
+ throw new Error(`服务器 "${name}" 已存在,请使用其他名称或先删除`);
167
+ }
168
+ config.remotes[name] = {
169
+ host,
170
+ user,
171
+ port,
172
+ key: options.key,
173
+ password: options.password,
174
+ };
175
+ saveConfig(config);
176
+ // 创建数据目录
177
+ ensureRemoteDataDir(name);
178
+ }
179
+ /**
180
+ * 删除远程服务器
181
+ */
182
+ export function removeRemote(name) {
183
+ const config = getConfig();
184
+ if (!config.remotes || !config.remotes[name]) {
185
+ return false;
186
+ }
187
+ delete config.remotes[name];
188
+ saveConfig(config);
189
+ // 删除数据目录
190
+ const dataDir = getRemoteDataDir(name);
191
+ if (fs.existsSync(dataDir)) {
192
+ fs.rmSync(dataDir, { recursive: true });
193
+ }
194
+ return true;
195
+ }
196
+ /**
197
+ * 显示所有远程服务器
198
+ */
199
+ export function displayRemotes() {
200
+ const remotes = getRemotes();
201
+ const config = getConfig();
202
+ const colors = getColors();
203
+ const names = Object.keys(remotes);
204
+ console.log('');
205
+ if (names.length === 0) {
206
+ console.log(chalk.gray(' 暂无远程服务器'));
207
+ console.log('');
208
+ console.log(chalk.gray(' 使用 pls remote add <name> <user@host> 添加服务器'));
209
+ console.log('');
210
+ return;
211
+ }
212
+ console.log(chalk.bold('远程服务器:'));
213
+ console.log(chalk.gray('━'.repeat(60)));
214
+ for (const name of names) {
215
+ const remote = remotes[name];
216
+ const authType = remote.password ? '密码' : remote.key ? '密钥' : '默认密钥';
217
+ const isDefault = config.defaultRemote === name;
218
+ // 服务器名称,如果是默认则显示标记
219
+ if (isDefault) {
220
+ console.log(` ${chalk.hex(colors.primary)(name)} ${chalk.hex(colors.success)('(default)')}`);
221
+ }
222
+ else {
223
+ console.log(` ${chalk.hex(colors.primary)(name)}`);
224
+ }
225
+ console.log(` ${chalk.gray('→')} ${remote.user}@${remote.host}:${remote.port}`);
226
+ console.log(` ${chalk.gray('认证:')} ${authType}${remote.key ? ` (${remote.key})` : ''}`);
227
+ // 显示工作目录
228
+ if (remote.workDir) {
229
+ console.log(` ${chalk.gray('工作目录:')} ${remote.workDir}`);
230
+ }
231
+ // 检查是否有缓存的系统信息
232
+ const sysInfo = getRemoteSysInfo(name);
233
+ if (sysInfo) {
234
+ console.log(` ${chalk.gray('系统:')} ${sysInfo.os} ${sysInfo.osVersion} (${sysInfo.shell})`);
235
+ }
236
+ console.log('');
237
+ }
238
+ console.log(chalk.gray('━'.repeat(60)));
239
+ console.log(chalk.gray('使用: pls -r <name> <prompt> 在远程服务器执行'));
240
+ console.log('');
241
+ }
242
+ /**
243
+ * 设置远程服务器工作目录
244
+ */
245
+ export function setRemoteWorkDir(name, workDir) {
246
+ const config = getConfig();
247
+ if (!config.remotes || !config.remotes[name]) {
248
+ throw new Error(`远程服务器 "${name}" 不存在`);
249
+ }
250
+ // 清除工作目录
251
+ if (!workDir || workDir === '-') {
252
+ delete config.remotes[name].workDir;
253
+ }
254
+ else {
255
+ config.remotes[name].workDir = workDir;
256
+ }
257
+ saveConfig(config);
258
+ }
259
+ /**
260
+ * 获取远程服务器工作目录
261
+ */
262
+ export function getRemoteWorkDir(name) {
263
+ const remote = getRemote(name);
264
+ return remote?.workDir;
265
+ }
266
+ /**
267
+ * 读取密码(交互式)
268
+ */
269
+ async function readPassword(prompt) {
270
+ return new Promise((resolve) => {
271
+ const rl = readline.createInterface({
272
+ input: process.stdin,
273
+ output: process.stdout,
274
+ });
275
+ // 隐藏输入
276
+ const stdin = process.stdin;
277
+ if (stdin.isTTY) {
278
+ stdin.setRawMode(true);
279
+ }
280
+ process.stdout.write(prompt);
281
+ let password = '';
282
+ stdin.on('data', (char) => {
283
+ const c = char.toString();
284
+ switch (c) {
285
+ case '\n':
286
+ case '\r':
287
+ case '\u0004': // Ctrl+D
288
+ if (stdin.isTTY) {
289
+ stdin.setRawMode(false);
290
+ }
291
+ console.log('');
292
+ rl.close();
293
+ resolve(password);
294
+ break;
295
+ case '\u0003': // Ctrl+C
296
+ if (stdin.isTTY) {
297
+ stdin.setRawMode(false);
298
+ }
299
+ console.log('');
300
+ rl.close();
301
+ process.exit(0);
302
+ case '\u007F': // Backspace
303
+ if (password.length > 0) {
304
+ password = password.slice(0, -1);
305
+ process.stdout.write('\b \b');
306
+ }
307
+ break;
308
+ default:
309
+ password += c;
310
+ process.stdout.write('*');
311
+ break;
312
+ }
313
+ });
314
+ });
315
+ }
316
+ /**
317
+ * 构建 SSH 命令参数
318
+ * @param remote 远程服务器配置
319
+ * @param command 要执行的命令
320
+ * @param options.password 密码(用于首次建立连接)
321
+ * @param options.socketPath ControlMaster socket 路径
322
+ * @param options.isMaster 是否建立 master 连接
323
+ */
324
+ function buildSSHArgs(remote, command, options = {}) {
325
+ const args = [];
326
+ // 使用 sshpass 处理密码认证(仅在建立新连接时需要)
327
+ let cmd = 'ssh';
328
+ if (options.password) {
329
+ cmd = 'sshpass';
330
+ args.push('-p', options.password, 'ssh');
331
+ }
332
+ // SSH 选项
333
+ args.push('-o', 'StrictHostKeyChecking=accept-new');
334
+ args.push('-o', 'ConnectTimeout=10');
335
+ // ControlMaster 选项
336
+ if (options.socketPath) {
337
+ if (options.isMaster) {
338
+ // 建立 master 连接
339
+ args.push('-o', 'ControlMaster=yes');
340
+ args.push('-o', `ControlPersist=${SSH_CONTROL_PERSIST}`);
341
+ }
342
+ else {
343
+ // 复用已有连接
344
+ args.push('-o', 'ControlMaster=no');
345
+ }
346
+ args.push('-o', `ControlPath=${options.socketPath}`);
347
+ }
348
+ // 端口
349
+ if (remote.port !== 22) {
350
+ args.push('-p', remote.port.toString());
351
+ }
352
+ // 密钥
353
+ if (remote.key) {
354
+ const keyPath = remote.key.replace(/^~/, os.homedir());
355
+ args.push('-i', keyPath);
356
+ }
357
+ // 目标
358
+ args.push(`${remote.user}@${remote.host}`);
359
+ // 命令
360
+ args.push(command);
361
+ return { cmd, args };
362
+ }
363
+ /**
364
+ * 执行 SSH 命令的内部实现
365
+ */
366
+ function spawnSSH(cmd, args, options) {
367
+ return new Promise((resolve, reject) => {
368
+ let stdout = '';
369
+ let stderr = '';
370
+ const child = spawn(cmd, args, {
371
+ stdio: ['pipe', 'pipe', 'pipe'],
372
+ });
373
+ // 超时处理
374
+ let timeoutId = null;
375
+ if (options.timeout) {
376
+ timeoutId = setTimeout(() => {
377
+ child.kill('SIGTERM');
378
+ reject(new Error(`命令执行超时 (${options.timeout}ms)`));
379
+ }, options.timeout);
380
+ }
381
+ child.stdout.on('data', (data) => {
382
+ const str = data.toString();
383
+ stdout += str;
384
+ options.onStdout?.(str);
385
+ });
386
+ child.stderr.on('data', (data) => {
387
+ const str = data.toString();
388
+ stderr += str;
389
+ options.onStderr?.(str);
390
+ });
391
+ // 写入 stdin
392
+ if (options.stdin) {
393
+ child.stdin.write(options.stdin);
394
+ child.stdin.end();
395
+ }
396
+ child.on('close', (code) => {
397
+ if (timeoutId) {
398
+ clearTimeout(timeoutId);
399
+ }
400
+ resolve({
401
+ exitCode: code || 0,
402
+ stdout,
403
+ stderr,
404
+ output: stdout + stderr,
405
+ });
406
+ });
407
+ child.on('error', (err) => {
408
+ if (timeoutId) {
409
+ clearTimeout(timeoutId);
410
+ }
411
+ // 检查是否是 sshpass 未安装
412
+ if (err.message.includes('ENOENT') && cmd === 'sshpass') {
413
+ reject(new Error('密码认证需要安装 sshpass,请运行: brew install hudochenkov/sshpass/sshpass'));
414
+ }
415
+ else {
416
+ reject(err);
417
+ }
418
+ });
419
+ });
420
+ }
421
+ /**
422
+ * 通过 SSH 执行命令
423
+ * 使用 ControlMaster 实现连接复用,密码认证只需输入一次
424
+ */
425
+ export async function sshExec(name, command, options = {}) {
426
+ const remote = getRemote(name);
427
+ if (!remote) {
428
+ throw new Error(`远程服务器 "${name}" 不存在`);
429
+ }
430
+ // 确保数据目录存在
431
+ ensureRemoteDataDir(name);
432
+ const socketPath = getSSHSocketPath(name);
433
+ const masterActive = isControlMasterActive(name);
434
+ // 如果需要密码认证且没有活跃的 master 连接
435
+ if (remote.password && !masterActive) {
436
+ // 读取密码并建立 master 连接
437
+ const password = await readPassword(`${name} 密码: `);
438
+ // 建立 master 连接(执行一个简单命令来建立连接)
439
+ const { cmd: masterCmd, args: masterArgs } = buildSSHArgs(remote, 'true', {
440
+ password,
441
+ socketPath,
442
+ isMaster: true,
443
+ });
444
+ try {
445
+ const masterResult = await spawnSSH(masterCmd, masterArgs, { timeout: 30000 });
446
+ if (masterResult.exitCode !== 0) {
447
+ throw new Error(`SSH 连接失败: ${masterResult.stderr}`);
448
+ }
449
+ }
450
+ catch (err) {
451
+ throw err;
452
+ }
453
+ }
454
+ // 使用 ControlMaster 连接(或直接连接)执行命令
455
+ const useSocket = remote.password || isControlMasterActive(name);
456
+ const { cmd, args } = buildSSHArgs(remote, command, {
457
+ socketPath: useSocket ? socketPath : undefined,
458
+ isMaster: false,
459
+ });
460
+ return spawnSSH(cmd, args, options);
461
+ }
462
+ /**
463
+ * 测试远程连接
464
+ */
465
+ export async function testRemoteConnection(name) {
466
+ const colors = getColors();
467
+ try {
468
+ const result = await sshExec(name, 'echo "pls-connection-test"', { timeout: 15000 });
469
+ if (result.exitCode === 0 && result.stdout.includes('pls-connection-test')) {
470
+ return { success: true, message: chalk.hex(colors.success)('连接成功') };
471
+ }
472
+ else {
473
+ return { success: false, message: chalk.hex(colors.error)(`连接失败,退出码: ${result.exitCode}`) };
474
+ }
475
+ }
476
+ catch (error) {
477
+ const message = error instanceof Error ? error.message : String(error);
478
+ return { success: false, message: chalk.hex(colors.error)(`连接失败: ${message}`) };
479
+ }
480
+ }
481
+ // ================== 系统信息采集 ==================
482
+ /**
483
+ * 获取缓存的远程系统信息
484
+ */
485
+ export function getRemoteSysInfo(name) {
486
+ const dataDir = getRemoteDataDir(name);
487
+ const sysInfoPath = path.join(dataDir, 'sysinfo.json');
488
+ if (!fs.existsSync(sysInfoPath)) {
489
+ return null;
490
+ }
491
+ try {
492
+ const content = fs.readFileSync(sysInfoPath, 'utf-8');
493
+ return JSON.parse(content);
494
+ }
495
+ catch {
496
+ return null;
497
+ }
498
+ }
499
+ /**
500
+ * 保存远程系统信息
501
+ */
502
+ function saveRemoteSysInfo(name, sysInfo) {
503
+ ensureRemoteDataDir(name);
504
+ const dataDir = getRemoteDataDir(name);
505
+ const sysInfoPath = path.join(dataDir, 'sysinfo.json');
506
+ fs.writeFileSync(sysInfoPath, JSON.stringify(sysInfo, null, 2));
507
+ }
508
+ /**
509
+ * 采集远程系统信息
510
+ */
511
+ export async function collectRemoteSysInfo(name, force = false) {
512
+ // 检查缓存
513
+ if (!force) {
514
+ const cached = getRemoteSysInfo(name);
515
+ if (cached) {
516
+ // 检查缓存是否过期(7天)
517
+ const cachedAt = new Date(cached.cachedAt);
518
+ const now = new Date();
519
+ const daysDiff = (now.getTime() - cachedAt.getTime()) / (1000 * 60 * 60 * 24);
520
+ if (daysDiff < 7) {
521
+ return cached;
522
+ }
523
+ }
524
+ }
525
+ // 采集系统信息
526
+ const collectScript = `
527
+ echo "OS:$(uname -s)"
528
+ echo "OS_VERSION:$(uname -r)"
529
+ echo "SHELL:$(basename "$SHELL")"
530
+ echo "HOSTNAME:$(hostname)"
531
+ `.trim();
532
+ const result = await sshExec(name, collectScript, { timeout: 30000 });
533
+ if (result.exitCode !== 0) {
534
+ throw new Error(`无法采集系统信息: ${result.stderr}`);
535
+ }
536
+ // 解析输出
537
+ const lines = result.stdout.split('\n');
538
+ const info = {};
539
+ for (const line of lines) {
540
+ const colonIndex = line.indexOf(':');
541
+ if (colonIndex > 0) {
542
+ const key = line.substring(0, colonIndex).trim();
543
+ const value = line.substring(colonIndex + 1).trim();
544
+ info[key] = value;
545
+ }
546
+ }
547
+ const sysInfo = {
548
+ os: info['OS'] || 'unknown',
549
+ osVersion: info['OS_VERSION'] || 'unknown',
550
+ shell: info['SHELL'] || 'bash',
551
+ hostname: info['HOSTNAME'] || 'unknown',
552
+ cachedAt: new Date().toISOString(),
553
+ };
554
+ // 保存缓存
555
+ saveRemoteSysInfo(name, sysInfo);
556
+ return sysInfo;
557
+ }
558
+ /**
559
+ * 格式化远程系统信息供 AI 使用
560
+ */
561
+ export function formatRemoteSysInfoForAI(name, sysInfo) {
562
+ const remote = getRemote(name);
563
+ if (!remote)
564
+ return '';
565
+ let info = `【远程服务器信息】
566
+ 服务器: ${name} (${remote.user}@${remote.host})
567
+ 操作系统: ${sysInfo.os} ${sysInfo.osVersion}
568
+ Shell: ${sysInfo.shell}
569
+ 主机名: ${sysInfo.hostname}`;
570
+ // 如果有工作目录,告知 AI 当前工作目录(执行时会自动 cd)
571
+ if (remote.workDir) {
572
+ info += `\n当前工作目录: ${remote.workDir}`;
573
+ }
574
+ return info;
575
+ }
576
+ /**
577
+ * 批量远程执行命令
578
+ * 每个服务器单独生成命令,支持异构环境
579
+ */
580
+ export async function generateBatchRemoteCommands(serverNames, userPrompt, options = {}) {
581
+ const { generateMultiStepCommand } = await import('./multi-step.js');
582
+ const { fetchRemoteShellHistory } = await import('./remote-history.js');
583
+ // 1. 验证所有服务器是否存在
584
+ const invalidServers = serverNames.filter(name => !getRemote(name));
585
+ if (invalidServers.length > 0) {
586
+ throw new Error(`以下服务器不存在: ${invalidServers.join(', ')}`);
587
+ }
588
+ // 2. 并发采集所有服务器的系统信息
589
+ const servers = await Promise.all(serverNames.map(async (name) => ({
590
+ name,
591
+ sysInfo: await collectRemoteSysInfo(name),
592
+ shellHistory: await fetchRemoteShellHistory(name),
593
+ })));
594
+ // 3. 并发为每个服务器生成命令
595
+ const commandResults = await Promise.all(servers.map(async (server) => {
596
+ const remoteContext = {
597
+ name: server.name,
598
+ sysInfo: server.sysInfo,
599
+ shellHistory: server.shellHistory,
600
+ };
601
+ const result = await generateMultiStepCommand(userPrompt, [], // 批量执行不支持多步骤,只生成单个命令
602
+ { debug: options.debug, remoteContext });
603
+ return {
604
+ server: server.name,
605
+ command: result.stepData.command,
606
+ sysInfo: server.sysInfo,
607
+ };
608
+ }));
609
+ return commandResults;
610
+ }
611
+ /**
612
+ * 执行批量远程命令
613
+ */
614
+ export async function executeBatchRemoteCommands(commands) {
615
+ // 并发执行所有命令
616
+ const results = await Promise.all(commands.map(async ({ server, command, sysInfo }) => {
617
+ let stdout = '';
618
+ let stderr = '';
619
+ const result = await sshExec(server, command, {
620
+ onStdout: (data) => { stdout += data; },
621
+ onStderr: (data) => { stderr += data; },
622
+ });
623
+ return {
624
+ server,
625
+ command,
626
+ exitCode: result.exitCode,
627
+ stdout,
628
+ stderr,
629
+ output: stdout + stderr,
630
+ sysInfo,
631
+ };
632
+ }));
633
+ return results;
634
+ }
@@ -54,4 +54,57 @@ export declare function displayShellHistory(): void;
54
54
  * 清空 shell 历史
55
55
  */
56
56
  export declare function clearShellHistory(): void;
57
+ /**
58
+ * 检测远程服务器的 shell 类型
59
+ */
60
+ export declare function detectRemoteShell(sshExecFn: (cmd: string) => Promise<{
61
+ stdout: string;
62
+ exitCode: number;
63
+ }>): Promise<ShellType>;
64
+ /**
65
+ * 获取远程 shell 配置文件路径
66
+ */
67
+ export declare function getRemoteShellConfigPath(shellType: ShellType): string;
68
+ /**
69
+ * 生成远程 hook 脚本
70
+ */
71
+ export declare function generateRemoteHookScript(shellType: ShellType): string | null;
72
+ /**
73
+ * 检查远程 hook 是否已安装
74
+ */
75
+ export declare function checkRemoteHookInstalled(sshExecFn: (cmd: string) => Promise<{
76
+ stdout: string;
77
+ exitCode: number;
78
+ }>, configPath: string): Promise<boolean>;
79
+ /**
80
+ * 在远程服务器安装 shell hook
81
+ */
82
+ export declare function installRemoteShellHook(sshExecFn: (cmd: string) => Promise<{
83
+ stdout: string;
84
+ exitCode: number;
85
+ }>, shellType: ShellType): Promise<{
86
+ success: boolean;
87
+ message: string;
88
+ }>;
89
+ /**
90
+ * 从远程服务器卸载 shell hook
91
+ */
92
+ export declare function uninstallRemoteShellHook(sshExecFn: (cmd: string) => Promise<{
93
+ stdout: string;
94
+ exitCode: number;
95
+ }>, shellType: ShellType): Promise<{
96
+ success: boolean;
97
+ message: string;
98
+ }>;
99
+ /**
100
+ * 获取远程 hook 状态
101
+ */
102
+ export declare function getRemoteHookStatus(sshExecFn: (cmd: string) => Promise<{
103
+ stdout: string;
104
+ exitCode: number;
105
+ }>): Promise<{
106
+ installed: boolean;
107
+ shellType: ShellType;
108
+ configPath: string;
109
+ }>;
57
110
  export {};