@yivan-lab/pretty-please 1.0.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 (89) hide show
  1. package/README.md +381 -28
  2. package/bin/pls.tsx +1138 -109
  3. package/dist/bin/pls.d.ts +1 -1
  4. package/dist/bin/pls.js +994 -91
  5. package/dist/package.json +80 -0
  6. package/dist/src/ai.d.ts +1 -41
  7. package/dist/src/ai.js +9 -190
  8. package/dist/src/alias.d.ts +41 -0
  9. package/dist/src/alias.js +240 -0
  10. package/dist/src/builtin-detector.d.ts +14 -8
  11. package/dist/src/builtin-detector.js +36 -16
  12. package/dist/src/chat-history.d.ts +16 -11
  13. package/dist/src/chat-history.js +35 -4
  14. package/dist/src/components/Chat.js +5 -4
  15. package/dist/src/components/CodeColorizer.js +26 -20
  16. package/dist/src/components/CommandBox.js +3 -17
  17. package/dist/src/components/ConfirmationPrompt.d.ts +2 -1
  18. package/dist/src/components/ConfirmationPrompt.js +9 -4
  19. package/dist/src/components/Duration.js +2 -1
  20. package/dist/src/components/InlineRenderer.js +2 -1
  21. package/dist/src/components/MarkdownDisplay.js +2 -1
  22. package/dist/src/components/MultiStepCommandGenerator.d.ts +5 -1
  23. package/dist/src/components/MultiStepCommandGenerator.js +127 -14
  24. package/dist/src/components/TableRenderer.js +2 -1
  25. package/dist/src/config.d.ts +59 -9
  26. package/dist/src/config.js +147 -48
  27. package/dist/src/history.d.ts +19 -5
  28. package/dist/src/history.js +26 -11
  29. package/dist/src/mastra-agent.d.ts +0 -1
  30. package/dist/src/mastra-agent.js +3 -4
  31. package/dist/src/mastra-chat.d.ts +28 -0
  32. package/dist/src/mastra-chat.js +93 -0
  33. package/dist/src/multi-step.d.ts +23 -7
  34. package/dist/src/multi-step.js +29 -6
  35. package/dist/src/prompts.d.ts +11 -0
  36. package/dist/src/prompts.js +140 -0
  37. package/dist/src/remote-history.d.ts +63 -0
  38. package/dist/src/remote-history.js +315 -0
  39. package/dist/src/remote.d.ts +113 -0
  40. package/dist/src/remote.js +634 -0
  41. package/dist/src/shell-hook.d.ts +87 -12
  42. package/dist/src/shell-hook.js +315 -17
  43. package/dist/src/sysinfo.d.ts +9 -5
  44. package/dist/src/sysinfo.js +2 -2
  45. package/dist/src/ui/theme.d.ts +27 -24
  46. package/dist/src/ui/theme.js +71 -21
  47. package/dist/src/upgrade.d.ts +41 -0
  48. package/dist/src/upgrade.js +348 -0
  49. package/dist/src/utils/console.d.ts +11 -11
  50. package/dist/src/utils/console.js +26 -17
  51. package/package.json +11 -9
  52. package/src/alias.ts +301 -0
  53. package/src/builtin-detector.ts +126 -0
  54. package/src/chat-history.ts +140 -0
  55. package/src/components/Chat.tsx +6 -5
  56. package/src/components/CodeColorizer.tsx +27 -19
  57. package/src/components/CommandBox.tsx +3 -17
  58. package/src/components/ConfirmationPrompt.tsx +11 -3
  59. package/src/components/Duration.tsx +2 -1
  60. package/src/components/InlineRenderer.tsx +2 -1
  61. package/src/components/MarkdownDisplay.tsx +2 -1
  62. package/src/components/MultiStepCommandGenerator.tsx +167 -16
  63. package/src/components/TableRenderer.tsx +2 -1
  64. package/src/config.ts +394 -0
  65. package/src/history.ts +160 -0
  66. package/src/mastra-agent.ts +3 -4
  67. package/src/mastra-chat.ts +124 -0
  68. package/src/multi-step.ts +45 -8
  69. package/src/prompts.ts +154 -0
  70. package/src/remote-history.ts +390 -0
  71. package/src/remote.ts +800 -0
  72. package/src/shell-hook.ts +754 -0
  73. package/src/{sysinfo.js → sysinfo.ts} +28 -16
  74. package/src/ui/theme.ts +101 -24
  75. package/src/upgrade.ts +397 -0
  76. package/src/utils/{console.js → console.ts} +36 -27
  77. package/bin/pls.js +0 -681
  78. package/src/ai.js +0 -324
  79. package/src/builtin-detector.js +0 -98
  80. package/src/chat-history.js +0 -94
  81. package/src/components/ChatStatus.tsx +0 -53
  82. package/src/components/CommandGenerator.tsx +0 -184
  83. package/src/components/ConfigDisplay.tsx +0 -64
  84. package/src/components/ConfigWizard.tsx +0 -101
  85. package/src/components/HistoryDisplay.tsx +0 -69
  86. package/src/components/HookManager.tsx +0 -150
  87. package/src/config.js +0 -221
  88. package/src/history.js +0 -131
  89. package/src/shell-hook.js +0 -393
package/bin/pls.tsx CHANGED
@@ -1,6 +1,4 @@
1
- #!/usr/bin/env node
2
- import React from 'react'
3
- import { render } from 'ink'
1
+ #!/usr/bin/env tsx
4
2
  import { Command } from 'commander'
5
3
  import { fileURLToPath } from 'url'
6
4
  import { dirname, join } from 'path'
@@ -8,12 +6,14 @@ import { exec } from 'child_process'
8
6
  import fs from 'fs'
9
7
  import os from 'os'
10
8
  import chalk from 'chalk'
11
- import { CommandGenerator } from '../src/components/CommandGenerator.js'
12
- import { MultiStepCommandGenerator } from '../src/components/MultiStepCommandGenerator.js'
13
- import { Chat } from '../src/components/Chat.js'
9
+ // React Ink 懒加载(只在需要 UI 时加载)
10
+ // import React from 'react'
11
+ // import { render } from 'ink'
12
+ // import { MultiStepCommandGenerator } from '../src/components/MultiStepCommandGenerator.js'
13
+ // import { Chat } from '../src/components/Chat.js'
14
14
  import { isConfigValid, setConfigValue, getConfig, maskApiKey } from '../src/config.js'
15
15
  import { clearHistory, addHistory, getHistory, getHistoryFilePath } from '../src/history.js'
16
- import { clearChatHistory, getChatRoundCount, getChatHistoryFilePath } from '../src/chat-history.js'
16
+ import { clearChatHistory, getChatRoundCount, getChatHistoryFilePath, displayChatHistory } from '../src/chat-history.js'
17
17
  import { type ExecutedStep } from '../src/multi-step.js'
18
18
  import {
19
19
  installShellHook,
@@ -21,55 +21,146 @@ import {
21
21
  getHookStatus,
22
22
  detectShell,
23
23
  getShellConfigPath,
24
+ displayShellHistory,
25
+ clearShellHistory,
24
26
  } from '../src/shell-hook.js'
27
+ import {
28
+ checkForUpdates,
29
+ showUpdateNotice,
30
+ performUpgrade,
31
+ } from '../src/upgrade.js'
32
+ import { getCurrentTheme } from '../src/ui/theme.js'
33
+ import {
34
+ addAlias,
35
+ removeAlias,
36
+ displayAliases,
37
+ resolveAlias,
38
+ } from '../src/alias.js'
39
+ import {
40
+ addRemote,
41
+ removeRemote,
42
+ displayRemotes,
43
+ getRemote,
44
+ testRemoteConnection,
45
+ sshExec,
46
+ collectRemoteSysInfo,
47
+ setRemoteWorkDir,
48
+ getRemoteWorkDir,
49
+ generateBatchRemoteCommands,
50
+ executeBatchRemoteCommands,
51
+ } from '../src/remote.js'
52
+ import {
53
+ addRemoteHistory,
54
+ displayRemoteHistory,
55
+ clearRemoteHistory,
56
+ fetchRemoteShellHistory,
57
+ displayRemoteShellHistory,
58
+ clearRemoteShellHistory,
59
+ } from '../src/remote-history.js'
60
+ import {
61
+ detectRemoteShell,
62
+ getRemoteShellConfigPath,
63
+ installRemoteShellHook,
64
+ uninstallRemoteShellHook,
65
+ getRemoteHookStatus,
66
+ } from '../src/shell-hook.js'
67
+
68
+ // 获取主题颜色的辅助函数
69
+ function getThemeColors() {
70
+ const theme = getCurrentTheme()
71
+ return {
72
+ primary: theme.primary,
73
+ success: theme.success,
74
+ error: theme.error,
75
+ warning: theme.warning,
76
+ info: theme.info,
77
+ muted: theme.text.muted,
78
+ secondary: theme.text.secondary,
79
+ }
80
+ }
25
81
  import * as console2 from '../src/utils/console.js'
82
+ // 导入 package.json(Bun 会自动打包进二进制)
83
+ import packageJson from '../package.json' with { type: 'json' }
26
84
 
27
- // 获取 package.json 版本
85
+ // 保留这些用于其他可能的用途
28
86
  const __filename = fileURLToPath(import.meta.url)
29
87
  const __dirname = dirname(__filename)
30
- const packageJson = JSON.parse(fs.readFileSync(join(__dirname, '../package.json'), 'utf-8'))
31
88
 
32
89
  const program = new Command()
33
90
 
91
+
92
+ // 启动时异步检查更新(不阻塞主流程)
93
+ let updateCheckResult: { hasUpdate: boolean; latestVersion: string | null } | null = null
94
+ const isUpgradeCommand = process.argv.includes('upgrade')
95
+
96
+ // 延迟更新检查到命令解析后(减少启动时间)
97
+ // 非 upgrade 命令时才检查更新
98
+ if (!isUpgradeCommand) {
99
+ // 延迟 100ms 开始检查,避免影响简单命令的响应速度
100
+ setTimeout(() => {
101
+ checkForUpdates(packageJson.version)
102
+ .then((result) => {
103
+ updateCheckResult = result
104
+ })
105
+ .catch(() => {
106
+ // 静默失败
107
+ })
108
+ }, 100)
109
+ }
110
+
111
+ // 程序退出时显示更新提示
112
+ process.on('beforeExit', () => {
113
+ if (updateCheckResult?.hasUpdate && updateCheckResult.latestVersion && !isUpgradeCommand) {
114
+ showUpdateNotice(packageJson.version, updateCheckResult.latestVersion)
115
+ }
116
+ })
117
+
34
118
  /**
35
119
  * 执行命令(原生版本)
36
120
  */
37
- function executeCommand(command: string, prompt: string): Promise<{ exitCode: number; output: string }> {
121
+ function executeCommand(command: string): Promise<{ exitCode: number; output: string; stdout: string; stderr: string }> {
38
122
  return new Promise((resolve) => {
39
- let output = ''
123
+ let stdout = ''
124
+ let stderr = ''
40
125
  let hasOutput = false
41
126
 
42
127
  console.log('') // 空行
43
- console2.printSeparator('输出')
44
128
 
45
- const child = exec(command)
129
+ // 计算命令框宽度,让分隔线长度一致
130
+ const lines = command.split('\n')
131
+ const maxContentWidth = Math.max(...lines.map(l => console2.getDisplayWidth(l)))
132
+ const boxWidth = Math.max(maxContentWidth + 4, console2.getDisplayWidth('生成命令') + 6, 20)
133
+ console2.printSeparator('输出', boxWidth)
134
+
135
+ // 使用 bash 并启用 pipefail,确保管道中任何命令失败都能正确返回非零退出码
136
+ const child = exec(`set -o pipefail; ${command}`, { shell: '/bin/bash' })
46
137
 
47
138
  child.stdout?.on('data', (data) => {
48
- output += data
139
+ stdout += data
49
140
  hasOutput = true
50
141
  process.stdout.write(data)
51
142
  })
52
143
 
53
144
  child.stderr?.on('data', (data) => {
54
- output += data
145
+ stderr += data
55
146
  hasOutput = true
56
147
  process.stderr.write(data)
57
148
  })
58
149
 
59
150
  child.on('close', (code) => {
60
151
  if (hasOutput) {
61
- console2.printSeparator('')
152
+ console2.printSeparator('', boxWidth)
62
153
  }
63
- resolve({ exitCode: code || 0, output })
154
+ resolve({ exitCode: code || 0, output: stdout + stderr, stdout, stderr })
64
155
  })
65
156
 
66
157
  child.on('error', (err) => {
67
158
  if (!hasOutput) {
68
- console2.printSeparator('')
159
+ console2.printSeparator('', boxWidth)
69
160
  }
70
161
  console2.error(err.message)
71
- console2.printSeparator('')
72
- resolve({ exitCode: 1, output: err.message })
162
+ console2.printSeparator('', boxWidth)
163
+ resolve({ exitCode: 1, output: err.message, stdout: '', stderr: err.message })
73
164
  })
74
165
  })
75
166
  }
@@ -80,6 +171,7 @@ program
80
171
  .description('AI 驱动的命令行工具,将自然语言转换为可执行的 Shell 命令')
81
172
  .version(packageJson.version, '-v, --version', '显示版本号')
82
173
  .helpOption('-h, --help', '显示帮助信息')
174
+ .allowUnknownOption(true) // 允许未知选项(用于别名参数传递)
83
175
 
84
176
  // config 子命令
85
177
  const configCmd = program.command('config').description('管理配置')
@@ -94,18 +186,30 @@ configCmd
94
186
 
95
187
  console.log('')
96
188
  console2.title('当前配置:')
97
- console2.muted('━'.repeat(40))
98
- console.log(` ${chalk.hex('#00D9FF')('apiKey')}: ${maskApiKey(config.apiKey)}`)
99
- console.log(` ${chalk.hex('#00D9FF')('baseUrl')}: ${config.baseUrl}`)
100
- console.log(` ${chalk.hex('#00D9FF')('provider')}: ${config.provider}`)
101
- console.log(` ${chalk.hex('#00D9FF')('model')}: ${config.model}`)
189
+ console2.muted('━'.repeat(50))
190
+ console.log(` ${chalk.hex(getThemeColors().primary)('apiKey')}: ${maskApiKey(config.apiKey)}`)
191
+ console.log(` ${chalk.hex(getThemeColors().primary)('baseUrl')}: ${config.baseUrl}`)
192
+ console.log(` ${chalk.hex(getThemeColors().primary)('provider')}: ${config.provider}`)
193
+ console.log(` ${chalk.hex(getThemeColors().primary)('model')}: ${config.model}`)
102
194
  console.log(
103
- ` ${chalk.hex('#00D9FF')('shellHook')}: ${
104
- config.shellHook ? chalk.hex('#10B981')('已启用') : chalk.gray('未启用')
195
+ ` ${chalk.hex(getThemeColors().primary)('shellHook')}: ${
196
+ config.shellHook ? chalk.hex(getThemeColors().success)('已启用') : chalk.gray('未启用')
105
197
  }`
106
198
  )
107
- console.log(` ${chalk.hex('#00D9FF')('chatHistoryLimit')}: ${config.chatHistoryLimit} 轮`)
108
- console2.muted(''.repeat(40))
199
+ console.log(
200
+ ` ${chalk.hex(getThemeColors().primary)('editMode')}: ${
201
+ config.editMode === 'auto' ? chalk.hex(getThemeColors().primary)('auto (自动编辑)') : chalk.gray('manual (按E编辑)')
202
+ }`
203
+ )
204
+ console.log(` ${chalk.hex(getThemeColors().primary)('chatHistoryLimit')}: ${config.chatHistoryLimit} 轮`)
205
+ console.log(` ${chalk.hex(getThemeColors().primary)('commandHistoryLimit')}: ${config.commandHistoryLimit} 条`)
206
+ console.log(` ${chalk.hex(getThemeColors().primary)('shellHistoryLimit')}: ${config.shellHistoryLimit} 条`)
207
+ console.log(
208
+ ` ${chalk.hex(getThemeColors().primary)('theme')}: ${
209
+ config.theme === 'dark' ? chalk.hex(getThemeColors().primary)('dark (深色)') : chalk.hex(getThemeColors().primary)('light (浅色)')
210
+ }`
211
+ )
212
+ console2.muted('━'.repeat(50))
109
213
  console2.muted(`配置文件: ${CONFIG_FILE}`)
110
214
  console.log('')
111
215
  })
@@ -133,6 +237,75 @@ configCmd.action(async () => {
133
237
  await runConfigWizard()
134
238
  })
135
239
 
240
+ // theme 子命令
241
+ const themeCmd = program.command('theme').description('管理主题')
242
+
243
+ themeCmd
244
+ .command('list')
245
+ .description('查看所有可用主题')
246
+ .action(async () => {
247
+ const { themes } = await import('../src/ui/theme.js')
248
+ const config = getConfig()
249
+ const currentTheme = config.theme || 'dark'
250
+
251
+ console.log('')
252
+ console2.title('🎨 可用主题:')
253
+ console2.muted('━'.repeat(50))
254
+
255
+ Object.keys(themes).forEach((themeName) => {
256
+ const isCurrent = themeName === currentTheme
257
+ const prefix = isCurrent ? '●' : '○'
258
+ const label = themeName === 'dark' ? 'dark (深色)' : 'light (浅色)'
259
+ const color = themeName === 'dark' ? '#00D9FF' : '#0284C7'
260
+
261
+ if (isCurrent) {
262
+ console.log(` ${chalk.hex(color)(prefix)} ${chalk.hex(color).bold(label)} ${chalk.gray('(当前)')}`)
263
+ } else {
264
+ console.log(` ${chalk.gray(prefix)} ${label}`)
265
+ }
266
+ })
267
+
268
+ console2.muted('━'.repeat(50))
269
+ console.log('')
270
+ })
271
+
272
+ themeCmd
273
+ .argument('[name]', '主题名称 (dark, light)')
274
+ .description('切换主题')
275
+ .action((name?: string) => {
276
+ if (!name) {
277
+ // 显示当前主题
278
+ const config = getConfig()
279
+ const currentTheme = config.theme || 'dark'
280
+ const label = currentTheme === 'dark' ? 'dark (深色)' : 'light (浅色)'
281
+ const color = currentTheme === 'dark' ? '#00D9FF' : '#0284C7'
282
+
283
+ console.log('')
284
+ console.log(`当前主题: ${chalk.hex(color).bold(label)}`)
285
+ console.log('')
286
+ console2.muted('使用 pls theme list 查看所有主题')
287
+ console2.muted('使用 pls theme <name> 切换主题')
288
+ console.log('')
289
+ return
290
+ }
291
+
292
+ // 切换主题
293
+ try {
294
+ setConfigValue('theme', name)
295
+ const label = name === 'dark' ? 'dark (深色)' : 'light (浅色)'
296
+ const color = name === 'dark' ? '#00D9FF' : '#0284C7'
297
+
298
+ console.log('')
299
+ console2.success(`已切换到 ${chalk.hex(color).bold(label)} 主题`)
300
+ console.log('')
301
+ } catch (error: any) {
302
+ console.log('')
303
+ console2.error(error.message)
304
+ console.log('')
305
+ process.exit(1)
306
+ }
307
+ })
308
+
136
309
  // history 子命令
137
310
  const historyCmd = program.command('history').description('查看或管理命令历史')
138
311
 
@@ -156,12 +329,20 @@ historyCmd
156
329
  history.forEach((item: any, index: number) => {
157
330
  const status = item.executed
158
331
  ? item.exitCode === 0
159
- ? chalk.hex('#10B981')('✓')
160
- : chalk.hex('#EF4444')(`✗ 退出码:${item.exitCode}`)
332
+ ? chalk.hex(getThemeColors().success)('✓')
333
+ : chalk.hex(getThemeColors().error)(`✗ 退出码:${item.exitCode}`)
161
334
  : chalk.gray('(未执行)')
162
335
 
163
- console.log(`\n${chalk.gray(`${index + 1}.`)} ${chalk.hex('#00D9FF')(item.userPrompt)}`)
164
- console.log(` ${chalk.dim('→')} ${item.command} ${status}`)
336
+ console.log(`\n${chalk.gray(`${index + 1}.`)} ${chalk.hex(getThemeColors().primary)(item.userPrompt)}`)
337
+
338
+ // 显示用户修改信息
339
+ if (item.userModified && item.aiGeneratedCommand) {
340
+ console.log(` ${chalk.dim('AI 生成:')} ${chalk.gray(item.aiGeneratedCommand)}`)
341
+ console.log(` ${chalk.dim('用户修改为:')} ${item.command} ${status} ${chalk.hex(getThemeColors().warning)('(已修改)')}`)
342
+ } else {
343
+ console.log(` ${chalk.dim('→')} ${item.command} ${status}`)
344
+ }
345
+
165
346
  console.log(` ${chalk.gray(item.timestamp)}`)
166
347
  })
167
348
 
@@ -180,7 +361,38 @@ historyCmd
180
361
  console.log('')
181
362
  })
182
363
 
183
- // 默认 history 命令(显示历史)
364
+ // history chat 子命令
365
+ const historyChatCmd = historyCmd.command('chat').description('查看或管理对话历史')
366
+
367
+ historyChatCmd.action(() => {
368
+ displayChatHistory()
369
+ })
370
+
371
+ historyChatCmd
372
+ .command('clear')
373
+ .description('清空对话历史')
374
+ .action(() => {
375
+ clearChatHistory()
376
+ console.log('')
377
+ console2.success('对话历史已清空')
378
+ console.log('')
379
+ })
380
+
381
+ // history shell 子命令
382
+ const historyShellCmd = historyCmd.command('shell').description('查看或管理 Shell 历史')
383
+
384
+ historyShellCmd.action(() => {
385
+ displayShellHistory()
386
+ })
387
+
388
+ historyShellCmd
389
+ .command('clear')
390
+ .description('清空 Shell 历史')
391
+ .action(() => {
392
+ clearShellHistory()
393
+ })
394
+
395
+ // 默认 history 命令(显示命令历史)
184
396
  historyCmd.action(() => {
185
397
  const history = getHistory()
186
398
 
@@ -198,12 +410,20 @@ historyCmd.action(() => {
198
410
  history.forEach((item: any, index: number) => {
199
411
  const status = item.executed
200
412
  ? item.exitCode === 0
201
- ? chalk.hex('#10B981')('✓')
202
- : chalk.hex('#EF4444')(`✗ 退出码:${item.exitCode}`)
413
+ ? chalk.hex(getThemeColors().success)('✓')
414
+ : chalk.hex(getThemeColors().error)(`✗ 退出码:${item.exitCode}`)
203
415
  : chalk.gray('(未执行)')
204
416
 
205
- console.log(`\n${chalk.gray(`${index + 1}.`)} ${chalk.hex('#00D9FF')(item.userPrompt)}`)
206
- console.log(` ${chalk.dim('→')} ${item.command} ${status}`)
417
+ console.log(`\n${chalk.gray(`${index + 1}.`)} ${chalk.hex(getThemeColors().primary)(item.userPrompt)}`)
418
+
419
+ // 显示用户修改信息
420
+ if (item.userModified && item.aiGeneratedCommand) {
421
+ console.log(` ${chalk.dim('AI 生成:')} ${chalk.gray(item.aiGeneratedCommand)}`)
422
+ console.log(` ${chalk.dim('用户修改为:')} ${item.command} ${status} ${chalk.hex(getThemeColors().warning)('(已修改)')}`)
423
+ } else {
424
+ console.log(` ${chalk.dim('→')} ${item.command} ${status}`)
425
+ }
426
+
207
427
  console.log(` ${chalk.gray(item.timestamp)}`)
208
428
  })
209
429
 
@@ -268,19 +488,19 @@ hookCmd
268
488
  console.log('')
269
489
  console2.title('📊 Shell Hook 状态')
270
490
  console2.muted('━'.repeat(40))
271
- console.log(` ${chalk.hex('#00D9FF')('Shell 类型')}: ${status.shellType}`)
272
- console.log(` ${chalk.hex('#00D9FF')('配置文件')}: ${status.configPath || '未知'}`)
491
+ console.log(` ${chalk.hex(getThemeColors().primary)('Shell 类型')}: ${status.shellType}`)
492
+ console.log(` ${chalk.hex(getThemeColors().primary)('配置文件')}: ${status.configPath || '未知'}`)
273
493
  console.log(
274
- ` ${chalk.hex('#00D9FF')('已安装')}: ${
275
- status.installed ? chalk.hex('#10B981')('是') : chalk.gray('否')
494
+ ` ${chalk.hex(getThemeColors().primary)('已安装')}: ${
495
+ status.installed ? chalk.hex(getThemeColors().success)('是') : chalk.gray('否')
276
496
  }`
277
497
  )
278
498
  console.log(
279
- ` ${chalk.hex('#00D9FF')('已启用')}: ${
280
- status.enabled ? chalk.hex('#10B981')('是') : chalk.gray('否')
499
+ ` ${chalk.hex(getThemeColors().primary)('已启用')}: ${
500
+ status.enabled ? chalk.hex(getThemeColors().success)('是') : chalk.gray('否')
281
501
  }`
282
502
  )
283
- console.log(` ${chalk.hex('#00D9FF')('历史文件')}: ${status.historyFile}`)
503
+ console.log(` ${chalk.hex(getThemeColors().primary)('历史文件')}: ${status.historyFile}`)
284
504
  console2.muted('━'.repeat(40))
285
505
 
286
506
  if (!status.installed) {
@@ -297,19 +517,19 @@ hookCmd.action(() => {
297
517
  console.log('')
298
518
  console2.title('📊 Shell Hook 状态')
299
519
  console2.muted('━'.repeat(40))
300
- console.log(` ${chalk.hex('#00D9FF')('Shell 类型')}: ${status.shellType}`)
301
- console.log(` ${chalk.hex('#00D9FF')('配置文件')}: ${status.configPath || '未知'}`)
520
+ console.log(` ${chalk.hex(getThemeColors().primary)('Shell 类型')}: ${status.shellType}`)
521
+ console.log(` ${chalk.hex(getThemeColors().primary)('配置文件')}: ${status.configPath || '未知'}`)
302
522
  console.log(
303
- ` ${chalk.hex('#00D9FF')('已安装')}: ${
304
- status.installed ? chalk.hex('#10B981')('是') : chalk.gray('否')
523
+ ` ${chalk.hex(getThemeColors().primary)('已安装')}: ${
524
+ status.installed ? chalk.hex(getThemeColors().success)('是') : chalk.gray('否')
305
525
  }`
306
526
  )
307
527
  console.log(
308
- ` ${chalk.hex('#00D9FF')('已启用')}: ${
309
- status.enabled ? chalk.hex('#10B981')('是') : chalk.gray('否')
528
+ ` ${chalk.hex(getThemeColors().primary)('已启用')}: ${
529
+ status.enabled ? chalk.hex(getThemeColors().success)('是') : chalk.gray('否')
310
530
  }`
311
531
  )
312
- console.log(` ${chalk.hex('#00D9FF')('历史文件')}: ${status.historyFile}`)
532
+ console.log(` ${chalk.hex(getThemeColors().primary)('历史文件')}: ${status.historyFile}`)
313
533
  console2.muted('━'.repeat(40))
314
534
 
315
535
  if (!status.installed) {
@@ -319,6 +539,429 @@ hookCmd.action(() => {
319
539
  console.log('')
320
540
  })
321
541
 
542
+ // upgrade 子命令
543
+ program
544
+ .command('upgrade')
545
+ .description('升级到最新版本')
546
+ .action(async () => {
547
+ const success = await performUpgrade(packageJson.version)
548
+ process.exit(success ? 0 : 1)
549
+ })
550
+
551
+ // alias 子命令
552
+ const aliasCmd = program.command('alias').description('管理命令别名')
553
+
554
+ // 获取所有子命令名称(用于检测冲突)
555
+ function getReservedCommands(): string[] {
556
+ return program.commands.map((cmd) => cmd.name())
557
+ }
558
+
559
+ aliasCmd
560
+ .command('list')
561
+ .description('列出所有别名')
562
+ .action(() => {
563
+ displayAliases()
564
+ })
565
+
566
+ aliasCmd
567
+ .command('add <name> <prompt>')
568
+ .description('添加别名(prompt 支持 {{param}} 或 {{param:default}} 参数模板)')
569
+ .option('-d, --description <desc>', '别名描述')
570
+ .action((name, prompt, options) => {
571
+ try {
572
+ addAlias(name, prompt, options.description, getReservedCommands())
573
+ console.log('')
574
+ console2.success(`已添加别名: ${name}`)
575
+ console.log(` ${chalk.gray('→')} ${prompt}`)
576
+ console.log('')
577
+ } catch (error: any) {
578
+ console.log('')
579
+ console2.error(error.message)
580
+ console.log('')
581
+ process.exit(1)
582
+ }
583
+ })
584
+
585
+ aliasCmd
586
+ .command('remove <name>')
587
+ .description('删除别名')
588
+ .action((name) => {
589
+ const removed = removeAlias(name)
590
+ console.log('')
591
+ if (removed) {
592
+ console2.success(`已删除别名: ${name}`)
593
+ } else {
594
+ console2.error(`别名不存在: ${name}`)
595
+ }
596
+ console.log('')
597
+ })
598
+
599
+ // 默认 alias 命令(显示列表)
600
+ aliasCmd.action(() => {
601
+ displayAliases()
602
+ })
603
+
604
+ // remote 子命令
605
+ const remoteCmd = program.command('remote').description('管理远程服务器')
606
+
607
+ remoteCmd
608
+ .command('list')
609
+ .description('列出所有远程服务器')
610
+ .action(() => {
611
+ displayRemotes()
612
+ })
613
+
614
+ remoteCmd
615
+ .command('add <name> <host>')
616
+ .description('添加远程服务器(格式: user@host 或 user@host:port)')
617
+ .option('-k, --key <path>', 'SSH 私钥路径')
618
+ .option('-p, --password', '使用密码认证(每次执行时输入)')
619
+ .action((name, host, options) => {
620
+ try {
621
+ addRemote(name, host, { key: options.key, password: options.password })
622
+ console.log('')
623
+ console2.success(`已添加远程服务器: ${name}`)
624
+ console.log(` ${chalk.gray('→')} ${host}`)
625
+ if (options.key) {
626
+ console.log(` ${chalk.gray('密钥:')} ${options.key}`)
627
+ }
628
+ if (options.password) {
629
+ console.log(` ${chalk.gray('认证:')} 密码(每次执行时输入)`)
630
+ }
631
+ console.log('')
632
+ } catch (error: any) {
633
+ console.log('')
634
+ console2.error(error.message)
635
+ console.log('')
636
+ process.exit(1)
637
+ }
638
+ })
639
+
640
+ remoteCmd
641
+ .command('remove <name>')
642
+ .description('删除远程服务器')
643
+ .action((name) => {
644
+ const removed = removeRemote(name)
645
+ console.log('')
646
+ if (removed) {
647
+ console2.success(`已删除远程服务器: ${name}`)
648
+ } else {
649
+ console2.error(`远程服务器不存在: ${name}`)
650
+ }
651
+ console.log('')
652
+ })
653
+
654
+ remoteCmd
655
+ .command('test <name>')
656
+ .description('测试远程服务器连接')
657
+ .action(async (name) => {
658
+ const remote = getRemote(name)
659
+ if (!remote) {
660
+ console.log('')
661
+ console2.error(`远程服务器不存在: ${name}`)
662
+ console.log('')
663
+ process.exit(1)
664
+ }
665
+
666
+ console.log('')
667
+ console2.info(`正在测试连接 ${name} (${remote.user}@${remote.host}:${remote.port})...`)
668
+
669
+ const result = await testRemoteConnection(name)
670
+ console.log(` ${result.message}`)
671
+
672
+ if (result.success) {
673
+ // 采集系统信息
674
+ console2.info('正在采集系统信息...')
675
+ try {
676
+ const sysInfo = await collectRemoteSysInfo(name, true)
677
+ console.log(` ${chalk.gray('系统:')} ${sysInfo.os} ${sysInfo.osVersion}`)
678
+ console.log(` ${chalk.gray('Shell:')} ${sysInfo.shell}`)
679
+ console.log(` ${chalk.gray('主机名:')} ${sysInfo.hostname}`)
680
+ } catch (error: any) {
681
+ console2.warning(`无法采集系统信息: ${error.message}`)
682
+ }
683
+ }
684
+ console.log('')
685
+ })
686
+
687
+ // remote hook 子命令
688
+ const remoteHookCmd = remoteCmd.command('hook').description('管理远程服务器 Shell Hook')
689
+
690
+ remoteHookCmd
691
+ .command('install <name>')
692
+ .description('在远程服务器安装 Shell Hook')
693
+ .action(async (name) => {
694
+ const remote = getRemote(name)
695
+ if (!remote) {
696
+ console.log('')
697
+ console2.error(`远程服务器不存在: ${name}`)
698
+ console.log('')
699
+ process.exit(1)
700
+ }
701
+
702
+ console.log('')
703
+ console2.title('🔧 远程 Shell Hook 安装')
704
+ console2.muted('━'.repeat(40))
705
+ console2.info(`目标服务器: ${name} (${remote.user}@${remote.host})`)
706
+
707
+ try {
708
+ // 检测远程 shell 类型
709
+ const sshExecFn = async (cmd: string) => {
710
+ const result = await sshExec(name, cmd, { timeout: 30000 })
711
+ return { stdout: result.stdout, exitCode: result.exitCode }
712
+ }
713
+
714
+ const shellType = await detectRemoteShell(sshExecFn)
715
+ const configPath = getRemoteShellConfigPath(shellType)
716
+ console2.muted(`检测到 Shell: ${shellType}`)
717
+ console2.muted(`配置文件: ${configPath}`)
718
+ console.log('')
719
+
720
+ const result = await installRemoteShellHook(sshExecFn, shellType)
721
+ console.log(` ${result.message}`)
722
+
723
+ if (result.success) {
724
+ console.log('')
725
+ console2.warning('⚠️ 请在远程服务器重启终端或执行:')
726
+ console2.info(` source ${configPath}`)
727
+ }
728
+ } catch (error: any) {
729
+ console2.error(`安装失败: ${error.message}`)
730
+ }
731
+ console.log('')
732
+ })
733
+
734
+ remoteHookCmd
735
+ .command('uninstall <name>')
736
+ .description('从远程服务器卸载 Shell Hook')
737
+ .action(async (name) => {
738
+ const remote = getRemote(name)
739
+ if (!remote) {
740
+ console.log('')
741
+ console2.error(`远程服务器不存在: ${name}`)
742
+ console.log('')
743
+ process.exit(1)
744
+ }
745
+
746
+ console.log('')
747
+ console2.info(`正在从 ${name} 卸载 Shell Hook...`)
748
+
749
+ try {
750
+ const sshExecFn = async (cmd: string) => {
751
+ const result = await sshExec(name, cmd, { timeout: 30000 })
752
+ return { stdout: result.stdout, exitCode: result.exitCode }
753
+ }
754
+
755
+ const shellType = await detectRemoteShell(sshExecFn)
756
+ const result = await uninstallRemoteShellHook(sshExecFn, shellType)
757
+ console.log(` ${result.message}`)
758
+
759
+ if (result.success) {
760
+ console.log('')
761
+ console2.warning('⚠️ 请在远程服务器重启终端使其生效')
762
+ }
763
+ } catch (error: any) {
764
+ console2.error(`卸载失败: ${error.message}`)
765
+ }
766
+ console.log('')
767
+ })
768
+
769
+ remoteHookCmd
770
+ .command('status <name>')
771
+ .description('查看远程服务器 Shell Hook 状态')
772
+ .action(async (name) => {
773
+ const remote = getRemote(name)
774
+ if (!remote) {
775
+ console.log('')
776
+ console2.error(`远程服务器不存在: ${name}`)
777
+ console.log('')
778
+ process.exit(1)
779
+ }
780
+
781
+ console.log('')
782
+ console2.info(`正在检查 ${name} 的 Hook 状态...`)
783
+
784
+ try {
785
+ const sshExecFn = async (cmd: string) => {
786
+ const result = await sshExec(name, cmd, { timeout: 30000 })
787
+ return { stdout: result.stdout, exitCode: result.exitCode }
788
+ }
789
+
790
+ const status = await getRemoteHookStatus(sshExecFn)
791
+
792
+ console.log('')
793
+ console2.title(`📊 远程 Shell Hook 状态 - ${name}`)
794
+ console2.muted('━'.repeat(40))
795
+ console.log(` ${chalk.hex(getThemeColors().primary)('Shell 类型')}: ${status.shellType}`)
796
+ console.log(` ${chalk.hex(getThemeColors().primary)('配置文件')}: ${status.configPath}`)
797
+ console.log(
798
+ ` ${chalk.hex(getThemeColors().primary)('已安装')}: ${
799
+ status.installed ? chalk.hex(getThemeColors().success)('是') : chalk.gray('否')
800
+ }`
801
+ )
802
+ console2.muted('━'.repeat(40))
803
+
804
+ if (!status.installed) {
805
+ console.log('')
806
+ console2.muted(`提示: 运行 pls remote hook install ${name} 安装 Shell Hook`)
807
+ }
808
+ } catch (error: any) {
809
+ console2.error(`检查失败: ${error.message}`)
810
+ }
811
+ console.log('')
812
+ })
813
+
814
+ // remote history 子命令
815
+ const remoteHistoryCmd = remoteCmd.command('history').description('管理远程服务器历史记录')
816
+
817
+ remoteHistoryCmd
818
+ .command('show <name>')
819
+ .description('显示远程服务器命令历史')
820
+ .action((name) => {
821
+ displayRemoteHistory(name)
822
+ })
823
+
824
+ remoteHistoryCmd
825
+ .command('clear <name>')
826
+ .description('清空远程服务器命令历史')
827
+ .action((name) => {
828
+ clearRemoteHistory(name)
829
+ console.log('')
830
+ console2.success(`已清空服务器 "${name}" 的命令历史`)
831
+ console.log('')
832
+ })
833
+
834
+ remoteHistoryCmd
835
+ .command('shell <name>')
836
+ .description('显示远程服务器 Shell 历史')
837
+ .action(async (name) => {
838
+ await displayRemoteShellHistory(name)
839
+ })
840
+
841
+ remoteHistoryCmd
842
+ .command('shell-clear <name>')
843
+ .description('清空远程服务器 Shell 历史')
844
+ .action(async (name) => {
845
+ await clearRemoteShellHistory(name)
846
+ })
847
+
848
+ // remote default 子命令
849
+ remoteCmd
850
+ .command('default [name]')
851
+ .description('设置或查看默认远程服务器')
852
+ .option('-c, --clear', '清除默认服务器设置')
853
+ .action((name?: string, options?: { clear?: boolean }) => {
854
+ const config = getConfig()
855
+
856
+ // 清除默认
857
+ if (options?.clear) {
858
+ if (config.defaultRemote) {
859
+ setConfigValue('defaultRemote', '')
860
+ console.log('')
861
+ console2.success('已清除默认远程服务器')
862
+ console.log('')
863
+ } else {
864
+ console.log('')
865
+ console2.muted('当前没有设置默认远程服务器')
866
+ console.log('')
867
+ }
868
+ return
869
+ }
870
+
871
+ // 查看默认
872
+ if (!name) {
873
+ console.log('')
874
+ if (config.defaultRemote) {
875
+ const remote = getRemote(config.defaultRemote)
876
+ if (remote) {
877
+ console.log(`默认远程服务器: ${chalk.hex(getThemeColors().primary)(config.defaultRemote)}`)
878
+ console.log(` ${chalk.gray('→')} ${remote.user}@${remote.host}:${remote.port}`)
879
+ } else {
880
+ console2.warning(`默认服务器 "${config.defaultRemote}" 不存在,建议清除设置`)
881
+ console2.muted('运行 pls remote default --clear 清除')
882
+ }
883
+ } else {
884
+ console2.muted('当前没有设置默认远程服务器')
885
+ console2.muted('使用 pls remote default <name> 设置默认服务器')
886
+ }
887
+ console.log('')
888
+ return
889
+ }
890
+
891
+ // 设置默认
892
+ const remote = getRemote(name)
893
+ if (!remote) {
894
+ console.log('')
895
+ console2.error(`远程服务器不存在: ${name}`)
896
+ console2.muted('使用 pls remote list 查看所有服务器')
897
+ console.log('')
898
+ process.exit(1)
899
+ }
900
+
901
+ setConfigValue('defaultRemote', name)
902
+ console.log('')
903
+ console2.success(`已设置默认远程服务器: ${name}`)
904
+ console.log(` ${chalk.gray('→')} ${remote.user}@${remote.host}:${remote.port}`)
905
+ console2.muted('现在可以使用 pls -r <prompt> 直接在该服务器执行')
906
+ console.log('')
907
+ })
908
+
909
+ // remote workdir 子命令
910
+ remoteCmd
911
+ .command('workdir <name> [path]')
912
+ .description('设置或查看远程服务器的工作目录')
913
+ .option('-c, --clear', '清除工作目录设置')
914
+ .action((name: string, workdirPath?: string, options?: { clear?: boolean }) => {
915
+ const remote = getRemote(name)
916
+ if (!remote) {
917
+ console.log('')
918
+ console2.error(`远程服务器不存在: ${name}`)
919
+ console.log('')
920
+ process.exit(1)
921
+ }
922
+
923
+ // 清除工作目录
924
+ if (options?.clear) {
925
+ if (remote.workDir) {
926
+ setRemoteWorkDir(name, '-')
927
+ console.log('')
928
+ console2.success(`已清除 ${name} 的工作目录设置`)
929
+ console.log('')
930
+ } else {
931
+ console.log('')
932
+ console2.muted(`${name} 没有设置工作目录`)
933
+ console.log('')
934
+ }
935
+ return
936
+ }
937
+
938
+ // 查看工作目录
939
+ if (!workdirPath) {
940
+ console.log('')
941
+ if (remote.workDir) {
942
+ console.log(`${chalk.hex(getThemeColors().primary)(name)} 的工作目录:`)
943
+ console.log(` ${chalk.gray('→')} ${remote.workDir}`)
944
+ } else {
945
+ console2.muted(`${name} 没有设置工作目录`)
946
+ console2.muted(`使用 pls remote workdir ${name} <path> 设置工作目录`)
947
+ }
948
+ console.log('')
949
+ return
950
+ }
951
+
952
+ // 设置工作目录
953
+ setRemoteWorkDir(name, workdirPath)
954
+ console.log('')
955
+ console2.success(`已设置 ${name} 的工作目录: ${workdirPath}`)
956
+ console2.muted('现在在该服务器执行的命令会自动切换到此目录')
957
+ console.log('')
958
+ })
959
+
960
+ // 默认 remote 命令(显示列表)
961
+ remoteCmd.action(() => {
962
+ displayRemotes()
963
+ })
964
+
322
965
  // chat 子命令
323
966
  const chatCmd = program.command('chat').description('AI 对话模式,问答、讲解命令')
324
967
 
@@ -347,8 +990,8 @@ chatCmd
347
990
  console.log('')
348
991
  console2.title('💬 AI 对话模式')
349
992
  console2.muted('━'.repeat(40))
350
- console.log(` ${chalk.hex('#00D9FF')('当前对话轮数')}: ${roundCount}`)
351
- console.log(` ${chalk.hex('#00D9FF')('历史文件')}: ${historyFile}`)
993
+ console.log(` ${chalk.hex(getThemeColors().primary)('当前对话轮数')}: ${roundCount}`)
994
+ console.log(` ${chalk.hex(getThemeColors().primary)('历史文件')}: ${historyFile}`)
352
995
  console2.muted('━'.repeat(40))
353
996
  console.log('')
354
997
  console2.muted('用法:')
@@ -367,28 +1010,42 @@ chatCmd
367
1010
  process.exit(1)
368
1011
  }
369
1012
 
370
- // 使用 Ink 渲染对话(Chat 适合用 Ink 流式输出)
371
- render(
372
- <Chat
373
- prompt={prompt}
374
- debug={options.debug}
375
- showRoundCount={true}
376
- onComplete={() => process.exit(0)}
377
- />
378
- )
1013
+ // 懒加载 Chat 组件(避免启动时加载 React/Ink
1014
+ ;(async () => {
1015
+ const React = await import('react')
1016
+ const { render } = await import('ink')
1017
+ const { Chat } = await import('../src/components/Chat.js')
1018
+
1019
+ render(
1020
+ React.createElement(Chat, {
1021
+ prompt,
1022
+ debug: options.debug,
1023
+ showRoundCount: true,
1024
+ onComplete: () => process.exit(0),
1025
+ })
1026
+ )
1027
+ })()
379
1028
  })
380
1029
 
381
1030
  // 默认命令(执行 prompt)
382
1031
  program
383
1032
  .argument('[prompt...]', '自然语言描述你想执行的操作')
384
1033
  .option('-d, --debug', '显示调试信息(系统信息、完整 prompt 等)')
1034
+ .option('-r, --remote [name]', '在远程服务器上执行(不指定则使用默认服务器)')
385
1035
  .action((promptArgs, options) => {
1036
+ // 智能处理 -r 参数:如果 -r 后面的值不是已注册的服务器名,把它当作 prompt 的一部分
1037
+ if (typeof options.remote === 'string' && !getRemote(options.remote)) {
1038
+ // "查看当前目录" 不是服务器名,放回 prompt
1039
+ promptArgs.unshift(options.remote)
1040
+ options.remote = true // 改为使用默认服务器
1041
+ }
1042
+
386
1043
  if (promptArgs.length === 0) {
387
1044
  program.help()
388
1045
  return
389
1046
  }
390
1047
 
391
- const prompt = promptArgs.join(' ')
1048
+ let prompt = promptArgs.join(' ')
392
1049
 
393
1050
  if (!prompt.trim()) {
394
1051
  console.log('')
@@ -398,6 +1055,23 @@ program
398
1055
  process.exit(1)
399
1056
  }
400
1057
 
1058
+ // 尝试解析别名(支持 pls disk 和 pls @disk 两种格式)
1059
+ try {
1060
+ const aliasResult = resolveAlias(prompt)
1061
+ if (aliasResult.resolved) {
1062
+ prompt = aliasResult.prompt
1063
+ if (options.debug) {
1064
+ console.log('')
1065
+ console2.muted(`别名解析: ${aliasResult.aliasName} → ${prompt}`)
1066
+ }
1067
+ }
1068
+ } catch (error: any) {
1069
+ console.log('')
1070
+ console2.error(error.message)
1071
+ console.log('')
1072
+ process.exit(1)
1073
+ }
1074
+
401
1075
  // 检查配置
402
1076
  if (!isConfigValid()) {
403
1077
  console.log('')
@@ -407,26 +1081,215 @@ program
407
1081
  process.exit(1)
408
1082
  }
409
1083
 
410
- // 使用多步骤命令生成器(统一处理单步和多步)
1084
+ // 解析远程服务器名称
1085
+ // options.remote 可能是:
1086
+ // - undefined: 没有使用 -r
1087
+ // - true: 使用了 -r 但没有指定名称(使用默认)
1088
+ // - string: 使用了 -r 并指定了名称(支持逗号分隔的多个服务器)
1089
+ let remoteName: string | undefined
1090
+ let remoteNames: string[] | undefined // 批量执行时的服务器列表
1091
+ if (options.remote !== undefined) {
1092
+ if (options.remote === true) {
1093
+ // 使用默认服务器
1094
+ const config = getConfig()
1095
+ if (!config.defaultRemote) {
1096
+ console.log('')
1097
+ console2.error('未设置默认远程服务器')
1098
+ console2.muted('使用 pls remote default <name> 设置默认服务器')
1099
+ console2.muted('或使用 pls -r <name> <prompt> 指定服务器')
1100
+ console.log('')
1101
+ process.exit(1)
1102
+ }
1103
+ remoteName = config.defaultRemote
1104
+ } else {
1105
+ // 检查是否为批量执行(逗号分隔的服务器名)
1106
+ if (options.remote.includes(',')) {
1107
+ remoteNames = options.remote.split(',').map(s => s.trim()).filter(s => s.length > 0)
1108
+
1109
+ // 验证所有服务器是否存在
1110
+ const invalidServers = remoteNames!.filter(name => !getRemote(name))
1111
+ if (invalidServers.length > 0) {
1112
+ console.log('')
1113
+ console2.error(`以下服务器不存在: ${invalidServers.join(', ')}`)
1114
+ console2.muted('使用 pls remote list 查看所有服务器')
1115
+ console2.muted('使用 pls remote add <name> <user@host> 添加服务器')
1116
+ console.log('')
1117
+ process.exit(1)
1118
+ }
1119
+ } else {
1120
+ remoteName = options.remote
1121
+
1122
+ // 检查服务器是否存在
1123
+ const remote = getRemote(remoteName!)
1124
+ if (!remote) {
1125
+ console.log('')
1126
+ console2.error(`远程服务器不存在: ${remoteName}`)
1127
+ console2.muted('使用 pls remote add <name> <user@host> 添加服务器')
1128
+ console.log('')
1129
+ process.exit(1)
1130
+ }
1131
+ }
1132
+ }
1133
+ }
1134
+
1135
+ // 懒加载 MultiStepCommandGenerator 组件(避免启动时加载 React/Ink)
411
1136
  ;(async () => {
1137
+ // 批量远程执行模式
1138
+ if (remoteNames && remoteNames.length > 0) {
1139
+ console.log('')
1140
+ console2.info(`正在为 ${remoteNames.length} 台服务器生成命令...`)
1141
+ console.log('')
1142
+
1143
+ try {
1144
+ // 1. 并发生成命令
1145
+ const commands = await generateBatchRemoteCommands(remoteNames, prompt, { debug: options.debug })
1146
+
1147
+ // 2. 显示生成的命令
1148
+ console2.success('✓ 命令生成完成\n')
1149
+ const theme = getCurrentTheme()
1150
+ commands.forEach(({ server, command, sysInfo }) => {
1151
+ console.log(chalk.hex(theme.primary)(`${server}`) + chalk.gray(` (${sysInfo.os}):`))
1152
+ console.log(chalk.hex(theme.secondary)(` ${command}`))
1153
+ })
1154
+ console.log('')
1155
+
1156
+ // 3. 询问用户确认
1157
+ const readline = await import('readline')
1158
+ const rl = readline.createInterface({
1159
+ input: process.stdin,
1160
+ output: process.stdout,
1161
+ })
1162
+
1163
+ const confirmed = await new Promise<boolean>((resolve) => {
1164
+ console.log(chalk.gray(`将在 ${remoteNames!.length} 台服务器执行以上命令`))
1165
+ rl.question(chalk.gray('执行? [回车执行 / Ctrl+C 取消] '), (answer) => {
1166
+ rl.close()
1167
+ resolve(true)
1168
+ })
1169
+ })
1170
+
1171
+ if (!confirmed) {
1172
+ console.log('')
1173
+ console2.muted('已取消执行')
1174
+ console.log('')
1175
+ process.exit(0)
1176
+ }
1177
+
1178
+ // 4. 并发执行
1179
+ console.log('')
1180
+ console2.info('正在执行...')
1181
+ const results = await executeBatchRemoteCommands(commands)
1182
+
1183
+ // 5. 显示执行结果摘要
1184
+ console.log('')
1185
+ console2.info('执行完成:\n')
1186
+ results.forEach(({ server, exitCode }) => {
1187
+ const icon = exitCode === 0 ? '✓' : '✗'
1188
+ const color = exitCode === 0 ? theme.success : theme.error
1189
+ console.log(` ${chalk.hex(color)(icon)} ${server} ${chalk.gray(`(退出码: ${exitCode})`)}`)
1190
+ })
1191
+
1192
+ // 6. 显示每个服务器的详细输出
1193
+ console.log('')
1194
+ results.forEach(({ server, output }) => {
1195
+ console.log(chalk.hex(theme.primary)(`─── ${server} ───`))
1196
+ console.log(output || chalk.gray('(无输出)'))
1197
+ })
1198
+
1199
+ // 7. 记录到历史
1200
+ results.forEach(({ server, command, exitCode, output }) => {
1201
+ addRemoteHistory(server, {
1202
+ userPrompt: prompt,
1203
+ command,
1204
+ aiGeneratedCommand: command, // 批量执行无编辑功能
1205
+ userModified: false,
1206
+ executed: true,
1207
+ exitCode,
1208
+ output,
1209
+ })
1210
+ })
1211
+
1212
+ // 8. 根据结果决定退出码
1213
+ const allSuccess = results.every(r => r.exitCode === 0)
1214
+ const allFailed = results.every(r => r.exitCode !== 0)
1215
+ if (allFailed) {
1216
+ process.exit(2) // 全部失败
1217
+ } else if (!allSuccess) {
1218
+ process.exit(1) // 部分失败
1219
+ }
1220
+ process.exit(0) // 全部成功
1221
+ } catch (error: any) {
1222
+ console.log('')
1223
+ console2.error(`批量执行失败: ${error.message}`)
1224
+ console.log('')
1225
+ process.exit(1)
1226
+ }
1227
+ return
1228
+ }
1229
+
1230
+ // 单服务器执行模式
1231
+ const React = await import('react')
1232
+ const { render } = await import('ink')
1233
+ const { MultiStepCommandGenerator } = await import('../src/components/MultiStepCommandGenerator.js')
1234
+
1235
+ // 如果是远程模式,先获取远程上下文
1236
+ let remoteContext: {
1237
+ name: string
1238
+ sysInfo: Awaited<ReturnType<typeof collectRemoteSysInfo>>
1239
+ shellHistory: Awaited<ReturnType<typeof fetchRemoteShellHistory>>
1240
+ } | null = null
1241
+
1242
+ if (remoteName) {
1243
+ console.log('')
1244
+ console2.info(`正在连接远程服务器 ${remoteName}...`)
1245
+
1246
+ try {
1247
+ // 采集系统信息(使用缓存)
1248
+ const sysInfo = await collectRemoteSysInfo(remoteName)
1249
+ if (options.debug) {
1250
+ console2.muted(`系统: ${sysInfo.os} ${sysInfo.osVersion} (${sysInfo.shell})`)
1251
+ }
1252
+
1253
+ // 获取远程 shell 历史
1254
+ const shellHistory = await fetchRemoteShellHistory(remoteName)
1255
+ if (options.debug && shellHistory.length > 0) {
1256
+ console2.muted(`Shell 历史: ${shellHistory.length} 条`)
1257
+ }
1258
+
1259
+ remoteContext = { name: remoteName, sysInfo, shellHistory }
1260
+ console2.success(`已连接到 ${remoteName}`)
1261
+ } catch (error: any) {
1262
+ console2.error(`无法连接到 ${remoteName}: ${error.message}`)
1263
+ console.log('')
1264
+ process.exit(1)
1265
+ }
1266
+ }
1267
+
412
1268
  const executedSteps: ExecutedStep[] = []
413
1269
  let currentStepNumber = 1
1270
+ let lastStepFailed = false // 跟踪上一步是否失败
414
1271
 
415
1272
  while (true) {
416
1273
  let stepResult: any = null
417
1274
 
418
1275
  // 使用 Ink 渲染命令生成
419
1276
  const { waitUntilExit, unmount } = render(
420
- <MultiStepCommandGenerator
421
- prompt={prompt}
422
- debug={options.debug}
423
- previousSteps={executedSteps}
424
- currentStepNumber={currentStepNumber}
425
- onStepComplete={(res) => {
1277
+ React.createElement(MultiStepCommandGenerator, {
1278
+ prompt,
1279
+ debug: options.debug,
1280
+ previousSteps: executedSteps,
1281
+ currentStepNumber,
1282
+ remoteContext: remoteContext ? {
1283
+ name: remoteContext.name,
1284
+ sysInfo: remoteContext.sysInfo,
1285
+ shellHistory: remoteContext.shellHistory,
1286
+ } : undefined,
1287
+ isRemote: !!remoteName, // 远程执行时不检测 builtin
1288
+ onStepComplete: (res: any) => {
426
1289
  stepResult = res
427
1290
  unmount()
428
- }}
429
- />
1291
+ },
1292
+ })
430
1293
  )
431
1294
 
432
1295
  await waitUntilExit()
@@ -441,23 +1304,88 @@ program
441
1304
  }
442
1305
 
443
1306
  if (stepResult.hasBuiltin) {
444
- addHistory({
445
- userPrompt: currentStepNumber === 1 ? prompt : `[步骤${currentStepNumber}] ${prompt}`,
446
- command: stepResult.command,
447
- executed: false,
448
- exitCode: null,
449
- output: '',
450
- reason: 'builtin',
451
- })
1307
+ // 远程模式记录到远程历史
1308
+ if (remoteName) {
1309
+ addRemoteHistory(remoteName, {
1310
+ userPrompt: currentStepNumber === 1 ? prompt : `[步骤${currentStepNumber}] ${prompt}`,
1311
+ command: stepResult.command,
1312
+ aiGeneratedCommand: stepResult.aiGeneratedCommand,
1313
+ userModified: stepResult.userModified || false,
1314
+ executed: false,
1315
+ exitCode: null,
1316
+ output: '',
1317
+ reason: 'builtin',
1318
+ })
1319
+ } else {
1320
+ addHistory({
1321
+ userPrompt: currentStepNumber === 1 ? prompt : `[步骤${currentStepNumber}] ${prompt}`,
1322
+ command: stepResult.command,
1323
+ aiGeneratedCommand: stepResult.aiGeneratedCommand, // AI 原始命令
1324
+ userModified: stepResult.userModified || false,
1325
+ executed: false,
1326
+ exitCode: null,
1327
+ output: '',
1328
+ reason: 'builtin',
1329
+ })
1330
+ }
452
1331
  process.exit(0)
453
1332
  }
454
1333
 
455
1334
  if (stepResult.confirmed) {
456
- // 执行命令
1335
+ // 如果命令为空,说明 AI 决定放弃
1336
+ if (!stepResult.command || stepResult.command.trim() === '') {
1337
+ console.log('')
1338
+ if (stepResult.reasoning) {
1339
+ console2.info(`💡 AI 分析: ${stepResult.reasoning}`)
1340
+ }
1341
+ console2.muted('❌ AI 决定停止尝试,任务失败')
1342
+ console.log('')
1343
+ process.exit(1)
1344
+ }
1345
+
1346
+ // 特殊处理:如果上一步失败,且 AI 决定放弃(continue: false),直接显示原因并退出
1347
+ if (
1348
+ lastStepFailed &&
1349
+ stepResult.needsContinue === false &&
1350
+ stepResult.command.startsWith('echo')
1351
+ ) {
1352
+ console.log('')
1353
+ if (stepResult.reasoning) {
1354
+ console2.info(`💡 AI 分析: ${stepResult.reasoning}`)
1355
+ }
1356
+ console2.muted('❌ AI 决定停止尝试,任务失败')
1357
+ console.log('')
1358
+ process.exit(1)
1359
+ }
1360
+
1361
+ // 执行命令(本地或远程)
457
1362
  const execStart = Date.now()
458
- const { exitCode, output } = await executeCommand(stepResult.command, prompt)
1363
+ let exitCode: number
1364
+ let output: string
1365
+ let stdout: string
1366
+
1367
+ if (remoteName) {
1368
+ // 远程执行
1369
+ const result = await executeRemoteCommand(remoteName, stepResult.command)
1370
+ exitCode = result.exitCode
1371
+ output = result.output
1372
+ stdout = result.stdout
1373
+ } else {
1374
+ // 本地执行
1375
+ const result = await executeCommand(stepResult.command)
1376
+ exitCode = result.exitCode
1377
+ output = result.output
1378
+ stdout = result.stdout
1379
+ }
459
1380
  const execDuration = Date.now() - execStart
460
1381
 
1382
+ // 判断命令是否成功
1383
+ // 退出码 141 = 128 + 13 (SIGPIPE),是管道正常关闭时的信号
1384
+ // 例如:ps aux | head -3,head 读完 3 行就关闭管道,ps 收到 SIGPIPE
1385
+ // 但如果退出码是 141 且没有 stdout 输出,说明可能是真正的错误
1386
+ const isSigpipeWithOutput = exitCode === 141 && stdout.trim().length > 0
1387
+ const isSuccess = exitCode === 0 || isSigpipeWithOutput
1388
+
461
1389
  // 保存到执行历史
462
1390
  const executedStep: ExecutedStep = {
463
1391
  command: stepResult.command,
@@ -469,19 +1397,34 @@ program
469
1397
  }
470
1398
  executedSteps.push(executedStep)
471
1399
 
472
- // 记录到 pls 历史
473
- addHistory({
474
- userPrompt:
475
- currentStepNumber === 1 ? prompt : `[步骤${currentStepNumber}] ${stepResult.reasoning || prompt}`,
476
- command: stepResult.command,
477
- executed: true,
478
- exitCode,
479
- output,
480
- })
1400
+ // 记录到 pls 历史(远程模式记录到远程历史)
1401
+ if (remoteName) {
1402
+ addRemoteHistory(remoteName, {
1403
+ userPrompt:
1404
+ currentStepNumber === 1 ? prompt : `[步骤${currentStepNumber}] ${stepResult.reasoning || prompt}`,
1405
+ command: stepResult.command,
1406
+ aiGeneratedCommand: stepResult.aiGeneratedCommand,
1407
+ userModified: stepResult.userModified || false,
1408
+ executed: true,
1409
+ exitCode,
1410
+ output,
1411
+ })
1412
+ } else {
1413
+ addHistory({
1414
+ userPrompt:
1415
+ currentStepNumber === 1 ? prompt : `[步骤${currentStepNumber}] ${stepResult.reasoning || prompt}`,
1416
+ command: stepResult.command,
1417
+ aiGeneratedCommand: stepResult.aiGeneratedCommand, // AI 原始命令
1418
+ userModified: stepResult.userModified || false,
1419
+ executed: true,
1420
+ exitCode,
1421
+ output,
1422
+ })
1423
+ }
481
1424
 
482
1425
  // 显示结果
483
1426
  console.log('')
484
- if (exitCode === 0) {
1427
+ if (isSuccess) {
485
1428
  if (currentStepNumber === 1 && stepResult.needsContinue !== true) {
486
1429
  // 单步命令
487
1430
  console2.success(`执行完成 ${console2.formatDuration(execDuration)}`)
@@ -489,14 +1432,19 @@ program
489
1432
  // 多步命令
490
1433
  console2.success(`步骤 ${currentStepNumber} 执行完成 ${console2.formatDuration(execDuration)}`)
491
1434
  }
1435
+ lastStepFailed = false
492
1436
  } else {
493
- // 执行失败,但不立即退出,让 AI 分析错误并调整策略
1437
+ // 执行失败,标记状态
494
1438
  console2.error(
495
1439
  `步骤 ${currentStepNumber} 执行失败,退出码: ${exitCode} ${console2.formatDuration(execDuration)}`
496
1440
  )
497
1441
  console.log('')
498
1442
  console2.warning('正在请 AI 分析错误并调整策略...')
499
- // 不退出,继续循环,AI 会收到错误信息
1443
+ lastStepFailed = true
1444
+ // 继续循环,让 AI 分析错误
1445
+ console.log('')
1446
+ currentStepNumber++
1447
+ continue
500
1448
  }
501
1449
 
502
1450
  // 判断是否继续
@@ -511,30 +1459,111 @@ program
511
1459
 
512
1460
  console.log('')
513
1461
  currentStepNumber++
1462
+ } else if (!stepResult.confirmed && !stepResult.cancelled) {
1463
+ // AI 返回了结果但没有确认(空命令的情况)
1464
+ if (lastStepFailed && stepResult.reasoning) {
1465
+ console.log('')
1466
+ console2.info(`💡 AI 分析: ${stepResult.reasoning}`)
1467
+ console2.muted('❌ AI 决定停止尝试,任务失败')
1468
+ console.log('')
1469
+ process.exit(1)
1470
+ }
1471
+ // 其他情况也退出
1472
+ console.log('')
1473
+ console2.muted('任务结束')
1474
+ console.log('')
1475
+ process.exit(0)
514
1476
  }
515
1477
  }
516
1478
  })()
517
1479
  })
518
1480
 
1481
+ /**
1482
+ * 执行远程命令
1483
+ * 如果设置了工作目录,自动添加 cd 前缀
1484
+ */
1485
+ async function executeRemoteCommand(
1486
+ remoteName: string,
1487
+ command: string
1488
+ ): Promise<{ exitCode: number; output: string; stdout: string; stderr: string }> {
1489
+ let stdout = ''
1490
+ let stderr = ''
1491
+
1492
+ // 如果有工作目录,自动添加 cd 前缀
1493
+ const workDir = getRemoteWorkDir(remoteName)
1494
+ const actualCommand = workDir ? `cd ${workDir} && ${command}` : command
1495
+
1496
+ console.log('') // 空行
1497
+
1498
+ // 计算命令框宽度,让分隔线长度一致
1499
+ const lines = command.split('\n')
1500
+ const maxContentWidth = Math.max(...lines.map(l => console2.getDisplayWidth(l)))
1501
+ const boxWidth = Math.max(maxContentWidth + 4, console2.getDisplayWidth('生成命令') + 6, 20)
1502
+ console2.printSeparator(`远程输出 (${remoteName})`, boxWidth)
1503
+
1504
+ try {
1505
+ const result = await sshExec(remoteName, actualCommand, {
1506
+ onStdout: (data) => {
1507
+ stdout += data
1508
+ process.stdout.write(data)
1509
+ },
1510
+ onStderr: (data) => {
1511
+ stderr += data
1512
+ process.stderr.write(data)
1513
+ },
1514
+ })
1515
+
1516
+ if (stdout || stderr) {
1517
+ console2.printSeparator('', boxWidth)
1518
+ }
1519
+
1520
+ return {
1521
+ exitCode: result.exitCode,
1522
+ output: stdout + stderr,
1523
+ stdout,
1524
+ stderr,
1525
+ }
1526
+ } catch (error: any) {
1527
+ console2.printSeparator('', boxWidth)
1528
+ console2.error(error.message)
1529
+ return {
1530
+ exitCode: 1,
1531
+ output: error.message,
1532
+ stdout: '',
1533
+ stderr: error.message,
1534
+ }
1535
+ }
1536
+ }
1537
+
519
1538
  // 自定义帮助信息
520
1539
  program.addHelpText(
521
1540
  'after',
522
1541
  `
523
1542
  ${chalk.bold('示例:')}
524
- ${chalk.hex('#00D9FF')('pls 安装 git')} 让 AI 生成安装 git 的命令
525
- ${chalk.hex('#00D9FF')('pls 查找大于 100MB 的文件')} 查找大文件
526
- ${chalk.hex('#00D9FF')('pls 删除刚才创建的文件')} AI 会参考历史记录
527
- ${chalk.hex('#00D9FF')('pls --debug 压缩 logs 目录')} 显示调试信息
528
- ${chalk.hex('#00D9FF')('pls -m 删除当前目录的空文件夹')} 多步骤模式(AI 自动规划)
529
- ${chalk.hex('#00D9FF')('pls chat tar 命令怎么用')} AI 对话模式
530
- ${chalk.hex('#00D9FF')('pls chat clear')} 清空对话历史
531
- ${chalk.hex('#00D9FF')('pls history')} 查看 pls 命令历史
532
- ${chalk.hex('#00D9FF')('pls history clear')} 清空历史记录
533
- ${chalk.hex('#00D9FF')('pls hook')} 查看 shell hook 状态
534
- ${chalk.hex('#00D9FF')('pls hook install')} 安装 shell hook(增强功能)
535
- ${chalk.hex('#00D9FF')('pls hook uninstall')} 卸载 shell hook
536
- ${chalk.hex('#00D9FF')('pls config')} 交互式配置
537
- ${chalk.hex('#00D9FF')('pls config list')} 查看当前配置
1543
+ ${chalk.hex(getThemeColors().primary)('pls 安装 git')} 让 AI 生成安装 git 的命令
1544
+ ${chalk.hex(getThemeColors().primary)('pls 查找大于 100MB 的文件')} 查找大文件
1545
+ ${chalk.hex(getThemeColors().primary)('pls 删除刚才创建的文件')} AI 会参考历史记录
1546
+ ${chalk.hex(getThemeColors().primary)('pls --debug 压缩 logs 目录')} 显示调试信息
1547
+ ${chalk.hex(getThemeColors().primary)('pls chat tar 命令怎么用')} AI 对话模式
1548
+ ${chalk.hex(getThemeColors().primary)('pls chat clear')} 清空对话历史
1549
+ ${chalk.hex(getThemeColors().primary)('pls history')} 查看 pls 命令历史
1550
+ ${chalk.hex(getThemeColors().primary)('pls history clear')} 清空历史记录
1551
+ ${chalk.hex(getThemeColors().primary)('pls alias')} 查看命令别名
1552
+ ${chalk.hex(getThemeColors().primary)('pls alias add disk "查看磁盘"')} 添加别名
1553
+ ${chalk.hex(getThemeColors().primary)('pls disk')} 使用别名(等同于 pls @disk)
1554
+ ${chalk.hex(getThemeColors().primary)('pls hook')} 查看 shell hook 状态
1555
+ ${chalk.hex(getThemeColors().primary)('pls hook install')} 安装 shell hook(增强功能)
1556
+ ${chalk.hex(getThemeColors().primary)('pls hook uninstall')} 卸载 shell hook
1557
+ ${chalk.hex(getThemeColors().primary)('pls upgrade')} 升级到最新版本
1558
+ ${chalk.hex(getThemeColors().primary)('pls config')} 交互式配置
1559
+ ${chalk.hex(getThemeColors().primary)('pls config list')} 查看当前配置
1560
+
1561
+ ${chalk.bold('远程执行:')}
1562
+ ${chalk.hex(getThemeColors().primary)('pls remote')} 查看远程服务器列表
1563
+ ${chalk.hex(getThemeColors().primary)('pls remote add myserver root@1.2.3.4')} 添加服务器
1564
+ ${chalk.hex(getThemeColors().primary)('pls remote test myserver')} 测试连接
1565
+ ${chalk.hex(getThemeColors().primary)('pls -r myserver 查看磁盘')} 在远程服务器执行
1566
+ ${chalk.hex(getThemeColors().primary)('pls remote hook install myserver')} 安装远程 Shell Hook
538
1567
  `
539
1568
  )
540
1569