@yivan-lab/pretty-please 1.1.0 → 1.3.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 (62) hide show
  1. package/README.md +390 -1
  2. package/bin/pls.tsx +1255 -123
  3. package/dist/bin/pls.js +1098 -103
  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 +54 -26
  9. package/dist/src/components/CodeColorizer.js +26 -20
  10. package/dist/src/components/CommandBox.js +19 -8
  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 +33 -3
  19. package/dist/src/config.js +83 -34
  20. package/dist/src/mastra-agent.d.ts +1 -0
  21. package/dist/src/mastra-agent.js +3 -11
  22. package/dist/src/mastra-chat.d.ts +13 -6
  23. package/dist/src/mastra-chat.js +31 -31
  24. package/dist/src/multi-step.d.ts +23 -7
  25. package/dist/src/multi-step.js +45 -26
  26. package/dist/src/prompts.d.ts +30 -4
  27. package/dist/src/prompts.js +218 -70
  28. package/dist/src/remote-history.d.ts +63 -0
  29. package/dist/src/remote-history.js +315 -0
  30. package/dist/src/remote.d.ts +113 -0
  31. package/dist/src/remote.js +634 -0
  32. package/dist/src/shell-hook.d.ts +58 -0
  33. package/dist/src/shell-hook.js +295 -26
  34. package/dist/src/ui/theme.d.ts +60 -23
  35. package/dist/src/ui/theme.js +544 -22
  36. package/dist/src/upgrade.d.ts +41 -0
  37. package/dist/src/upgrade.js +348 -0
  38. package/dist/src/utils/console.d.ts +4 -0
  39. package/dist/src/utils/console.js +89 -17
  40. package/package.json +4 -4
  41. package/src/alias.ts +301 -0
  42. package/src/chat-history.ts +11 -1
  43. package/src/components/Chat.tsx +71 -26
  44. package/src/components/CodeColorizer.tsx +27 -19
  45. package/src/components/CommandBox.tsx +26 -8
  46. package/src/components/ConfirmationPrompt.tsx +2 -1
  47. package/src/components/Duration.tsx +2 -1
  48. package/src/components/InlineRenderer.tsx +2 -1
  49. package/src/components/MarkdownDisplay.tsx +2 -1
  50. package/src/components/MultiStepCommandGenerator.tsx +25 -11
  51. package/src/components/TableRenderer.tsx +2 -1
  52. package/src/config.ts +126 -35
  53. package/src/mastra-agent.ts +3 -12
  54. package/src/mastra-chat.ts +40 -34
  55. package/src/multi-step.ts +62 -30
  56. package/src/prompts.ts +236 -78
  57. package/src/remote-history.ts +390 -0
  58. package/src/remote.ts +800 -0
  59. package/src/shell-hook.ts +339 -26
  60. package/src/ui/theme.ts +632 -23
  61. package/src/upgrade.ts +397 -0
  62. package/src/utils/console.ts +99 -17
package/src/shell-hook.ts CHANGED
@@ -4,9 +4,21 @@ import os from 'os'
4
4
  import chalk from 'chalk'
5
5
  import { CONFIG_DIR, getConfig, setConfigValue } from './config.js'
6
6
  import { getHistory } from './history.js'
7
+ import { getCurrentTheme } from './ui/theme.js'
8
+
9
+ // 获取主题颜色
10
+ function getColors() {
11
+ const theme = getCurrentTheme()
12
+ return {
13
+ primary: theme.primary,
14
+ success: theme.success,
15
+ error: theme.error,
16
+ warning: theme.warning,
17
+ secondary: theme.secondary,
18
+ }
19
+ }
7
20
 
8
21
  const SHELL_HISTORY_FILE = path.join(CONFIG_DIR, 'shell_history.jsonl')
9
- const MAX_SHELL_HISTORY = 20
10
22
 
11
23
  // Hook 标记,用于识别我们添加的内容
12
24
  const HOOK_START_MARKER = '# >>> pretty-please shell hook >>>'
@@ -73,6 +85,9 @@ export function getShellConfigPath(shellType: ShellType): string | null {
73
85
  * 生成 zsh hook 脚本
74
86
  */
75
87
  function generateZshHook(): string {
88
+ const config = getConfig()
89
+ const limit = config.shellHistoryLimit || 10 // 从配置读取
90
+
76
91
  return `
77
92
  ${HOOK_START_MARKER}
78
93
  # 记录命令到 pretty-please 历史
@@ -89,8 +104,8 @@ __pls_precmd() {
89
104
  # 转义命令中的特殊字符
90
105
  local escaped_cmd=$(echo "$__PLS_LAST_CMD" | sed 's/\\\\/\\\\\\\\/g; s/"/\\\\"/g')
91
106
  echo "{\\"cmd\\":\\"$escaped_cmd\\",\\"exit\\":$exit_code,\\"time\\":\\"$timestamp\\"}" >> "${CONFIG_DIR}/shell_history.jsonl"
92
- # 保持文件不超过 ${MAX_SHELL_HISTORY}
93
- tail -n ${MAX_SHELL_HISTORY} "${CONFIG_DIR}/shell_history.jsonl" > "${CONFIG_DIR}/shell_history.jsonl.tmp" && mv "${CONFIG_DIR}/shell_history.jsonl.tmp" "${CONFIG_DIR}/shell_history.jsonl"
107
+ # 保持文件不超过 ${limit} 行(从配置读取)
108
+ tail -n ${limit} "${CONFIG_DIR}/shell_history.jsonl" > "${CONFIG_DIR}/shell_history.jsonl.tmp" && mv "${CONFIG_DIR}/shell_history.jsonl.tmp" "${CONFIG_DIR}/shell_history.jsonl"
94
109
  unset __PLS_LAST_CMD
95
110
  fi
96
111
  }
@@ -106,6 +121,9 @@ ${HOOK_END_MARKER}
106
121
  * 生成 bash hook 脚本
107
122
  */
108
123
  function generateBashHook(): string {
124
+ const config = getConfig()
125
+ const limit = config.shellHistoryLimit || 10 // 从配置读取
126
+
109
127
  return `
110
128
  ${HOOK_START_MARKER}
111
129
  # 记录命令到 pretty-please 历史
@@ -117,7 +135,7 @@ __pls_prompt_command() {
117
135
  local timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
118
136
  local escaped_cmd=$(echo "$last_cmd" | sed 's/\\\\/\\\\\\\\/g; s/"/\\\\"/g')
119
137
  echo "{\\"cmd\\":\\"$escaped_cmd\\",\\"exit\\":$exit_code,\\"time\\":\\"$timestamp\\"}" >> "${CONFIG_DIR}/shell_history.jsonl"
120
- tail -n ${MAX_SHELL_HISTORY} "${CONFIG_DIR}/shell_history.jsonl" > "${CONFIG_DIR}/shell_history.jsonl.tmp" && mv "${CONFIG_DIR}/shell_history.jsonl.tmp" "${CONFIG_DIR}/shell_history.jsonl"
138
+ tail -n ${limit} "${CONFIG_DIR}/shell_history.jsonl" > "${CONFIG_DIR}/shell_history.jsonl.tmp" && mv "${CONFIG_DIR}/shell_history.jsonl.tmp" "${CONFIG_DIR}/shell_history.jsonl"
121
139
  fi
122
140
  }
123
141
 
@@ -132,6 +150,9 @@ ${HOOK_END_MARKER}
132
150
  * 生成 PowerShell hook 脚本
133
151
  */
134
152
  function generatePowerShellHook(): string {
153
+ const config = getConfig()
154
+ const limit = config.shellHistoryLimit || 10 // 从配置读取
155
+
135
156
  return `
136
157
  ${HOOK_START_MARKER}
137
158
  # 记录命令到 pretty-please 历史
@@ -147,8 +168,8 @@ function __Pls_RecordCommand {
147
168
  $escapedCmd = $lastCmd -replace '\\\\', '\\\\\\\\' -replace '"', '\\\\"'
148
169
  $json = "{\`"cmd\`":\`"$escapedCmd\`",\`"exit\`":$exitCode,\`"time\`":\`"$timestamp\`"}"
149
170
  Add-Content -Path "${CONFIG_DIR}/shell_history.jsonl" -Value $json
150
- # 保持文件不超过 ${MAX_SHELL_HISTORY}
151
- $content = Get-Content "${CONFIG_DIR}/shell_history.jsonl" -Tail ${MAX_SHELL_HISTORY}
171
+ # 保持文件不超过 ${limit} 行(从配置读取)
172
+ $content = Get-Content "${CONFIG_DIR}/shell_history.jsonl" -Tail ${limit}
152
173
  $content | Set-Content "${CONFIG_DIR}/shell_history.jsonl"
153
174
  }
154
175
  }
@@ -186,15 +207,16 @@ function generateHookScript(shellType: ShellType): string | null {
186
207
  export async function installShellHook(): Promise<boolean> {
187
208
  const shellType = detectShell()
188
209
  const configPath = getShellConfigPath(shellType)
210
+ const colors = getColors()
189
211
 
190
212
  if (!configPath) {
191
- console.log(chalk.red(`❌ 不支持的 shell 类型: ${shellType}`))
213
+ console.log(chalk.hex(colors.error)(`❌ 不支持的 shell 类型: ${shellType}`))
192
214
  return false
193
215
  }
194
216
 
195
217
  const hookScript = generateHookScript(shellType)
196
218
  if (!hookScript) {
197
- console.log(chalk.red(`❌ 无法为 ${shellType} 生成 hook 脚本`))
219
+ console.log(chalk.hex(colors.error)(`❌ 无法为 ${shellType} 生成 hook 脚本`))
198
220
  return false
199
221
  }
200
222
 
@@ -202,7 +224,7 @@ export async function installShellHook(): Promise<boolean> {
202
224
  if (fs.existsSync(configPath)) {
203
225
  const content = fs.readFileSync(configPath, 'utf-8')
204
226
  if (content.includes(HOOK_START_MARKER)) {
205
- console.log(chalk.yellow('⚠️ Shell hook 已安装,跳过'))
227
+ console.log(chalk.hex(colors.warning)('⚠️ Shell hook 已安装,跳过'))
206
228
  setConfigValue('shellHook', true)
207
229
  return true
208
230
  }
@@ -226,9 +248,9 @@ export async function installShellHook(): Promise<boolean> {
226
248
  // 更新配置
227
249
  setConfigValue('shellHook', true)
228
250
 
229
- console.log(chalk.green(`✅ Shell hook 已安装到: ${configPath}`))
230
- console.log(chalk.yellow('⚠️ 请重启终端或执行以下命令使其生效:'))
231
- console.log(chalk.cyan(` source ${configPath}`))
251
+ console.log(chalk.hex(colors.success)(`✅ Shell hook 已安装到: ${configPath}`))
252
+ console.log(chalk.hex(colors.warning)('⚠️ 请重启终端或执行以下命令使其生效:'))
253
+ console.log(chalk.hex(colors.primary)(` source ${configPath}`))
232
254
 
233
255
  return true
234
256
  }
@@ -239,9 +261,10 @@ export async function installShellHook(): Promise<boolean> {
239
261
  export function uninstallShellHook(): boolean {
240
262
  const shellType = detectShell()
241
263
  const configPath = getShellConfigPath(shellType)
264
+ const colors = getColors()
242
265
 
243
266
  if (!configPath || !fs.existsSync(configPath)) {
244
- console.log(chalk.yellow('⚠️ 未找到 shell 配置文件'))
267
+ console.log(chalk.hex(colors.warning)('⚠️ 未找到 shell 配置文件'))
245
268
  setConfigValue('shellHook', false)
246
269
  return true
247
270
  }
@@ -253,7 +276,7 @@ export function uninstallShellHook(): boolean {
253
276
  const endIndex = content.indexOf(HOOK_END_MARKER)
254
277
 
255
278
  if (startIndex === -1 || endIndex === -1) {
256
- console.log(chalk.yellow('⚠️ 未找到已安装的 hook'))
279
+ console.log(chalk.hex(colors.warning)('⚠️ 未找到已安装的 hook'))
257
280
  setConfigValue('shellHook', false)
258
281
  return true
259
282
  }
@@ -271,8 +294,8 @@ export function uninstallShellHook(): boolean {
271
294
  fs.unlinkSync(SHELL_HISTORY_FILE)
272
295
  }
273
296
 
274
- console.log(chalk.green('✅ Shell hook 已卸载'))
275
- console.log(chalk.yellow('⚠️ 请重启终端使其生效'))
297
+ console.log(chalk.hex(colors.success)('✅ Shell hook 已卸载'))
298
+ console.log(chalk.hex(colors.warning)('⚠️ 请重启终端使其生效'))
276
299
 
277
300
  return true
278
301
  }
@@ -299,7 +322,7 @@ export function getShellHistory(): ShellHistoryItem[] {
299
322
  .split('\n')
300
323
  .filter((line) => line.trim())
301
324
 
302
- return lines
325
+ const allHistory = lines
303
326
  .map((line) => {
304
327
  try {
305
328
  return JSON.parse(line) as ShellHistoryItem
@@ -308,6 +331,10 @@ export function getShellHistory(): ShellHistoryItem[] {
308
331
  }
309
332
  })
310
333
  .filter((item): item is ShellHistoryItem => item !== null)
334
+
335
+ // 应用 shellHistoryLimit 限制:只返回最近的 N 条
336
+ const limit = config.shellHistoryLimit || 15
337
+ return allHistory.slice(-limit)
311
338
  } catch {
312
339
  return []
313
340
  }
@@ -430,11 +457,12 @@ export function getHookStatus(): HookStatus {
430
457
  export function displayShellHistory(): void {
431
458
  const config = getConfig()
432
459
  const history = getShellHistory()
460
+ const colors = getColors()
433
461
 
434
462
  if (!config.shellHook) {
435
463
  console.log('')
436
- console.log(chalk.yellow('⚠️ Shell Hook 未启用'))
437
- console.log(chalk.gray('运行 ') + chalk.cyan('pls hook install') + chalk.gray(' 启用 Shell Hook'))
464
+ console.log(chalk.hex(colors.warning)('⚠️ Shell Hook 未启用'))
465
+ console.log(chalk.gray('运行 ') + chalk.hex(colors.primary)('pls hook install') + chalk.gray(' 启用 Shell Hook'))
438
466
  console.log('')
439
467
  return
440
468
  }
@@ -452,7 +480,7 @@ export function displayShellHistory(): void {
452
480
 
453
481
  history.forEach((item, index) => {
454
482
  const num = index + 1
455
- const status = item.exit === 0 ? chalk.green('✓') : chalk.red(`✗ (${item.exit})`)
483
+ const status = item.exit === 0 ? chalk.hex(colors.success)('✓') : chalk.hex(colors.error)(`✗ (${item.exit})`)
456
484
 
457
485
  // 检查是否是 pls 命令
458
486
  const isPls = item.cmd.startsWith('pls ') || item.cmd.startsWith('please ')
@@ -465,21 +493,21 @@ export function displayShellHistory(): void {
465
493
  if (plsRecord && plsRecord.executed) {
466
494
  // 检查用户是否修改了命令
467
495
  if (plsRecord.userModified && plsRecord.aiGeneratedCommand) {
468
- console.log(` ${chalk.cyan(num.toString().padStart(2, ' '))}. ${chalk.magenta('[pls]')} "${args}"`)
496
+ console.log(` ${chalk.hex(colors.primary)(num.toString().padStart(2, ' '))}. ${chalk.hex(colors.secondary)('[pls]')} "${args}"`)
469
497
  console.log(` ${chalk.dim('AI 生成:')} ${chalk.gray(plsRecord.aiGeneratedCommand)}`)
470
498
  console.log(
471
- ` ${chalk.dim('用户修改为:')} ${plsRecord.command} ${status} ${chalk.yellow('(已修改)')}`
499
+ ` ${chalk.dim('用户修改为:')} ${plsRecord.command} ${status} ${chalk.hex(colors.warning)('(已修改)')}`
472
500
  )
473
501
  } else {
474
502
  console.log(
475
- ` ${chalk.cyan(num.toString().padStart(2, ' '))}. ${chalk.magenta('[pls]')} "${args}" → ${plsRecord.command} ${status}`
503
+ ` ${chalk.hex(colors.primary)(num.toString().padStart(2, ' '))}. ${chalk.hex(colors.secondary)('[pls]')} "${args}" → ${plsRecord.command} ${status}`
476
504
  )
477
505
  }
478
506
  } else {
479
- console.log(` ${chalk.cyan(num.toString().padStart(2, ' '))}. ${chalk.magenta('[pls]')} ${args} ${status}`)
507
+ console.log(` ${chalk.hex(colors.primary)(num.toString().padStart(2, ' '))}. ${chalk.hex(colors.secondary)('[pls]')} ${args} ${status}`)
480
508
  }
481
509
  } else {
482
- console.log(` ${chalk.cyan(num.toString().padStart(2, ' '))}. ${item.cmd} ${status}`)
510
+ console.log(` ${chalk.hex(colors.primary)(num.toString().padStart(2, ' '))}. ${item.cmd} ${status}`)
483
511
  }
484
512
  })
485
513
 
@@ -489,6 +517,49 @@ export function displayShellHistory(): void {
489
517
  console.log('')
490
518
  }
491
519
 
520
+ /**
521
+ * 当 shellHistoryLimit 变化时,自动重装 Hook
522
+ * 返回是否成功重装
523
+ */
524
+ export async function reinstallHookForLimitChange(oldLimit: number, newLimit: number): Promise<boolean> {
525
+ const config = getConfig()
526
+
527
+ // 只有在 hook 已启用时才重装
528
+ if (!config.shellHook) {
529
+ return false
530
+ }
531
+
532
+ // 值没有变化,不需要重装
533
+ if (oldLimit === newLimit) {
534
+ return false
535
+ }
536
+
537
+ const colors = getColors()
538
+
539
+ console.log('')
540
+ console.log(chalk.hex(colors.primary)(`检测到 shellHistoryLimit 变化 (${oldLimit} → ${newLimit})`))
541
+ console.log(chalk.hex(colors.primary)('正在更新 Shell Hook...'))
542
+
543
+ uninstallShellHook()
544
+ await installShellHook()
545
+
546
+ console.log('')
547
+ console.log(chalk.hex(colors.warning)('⚠️ 请重启终端或运行以下命令使新配置生效:'))
548
+
549
+ const shellType = detectShell()
550
+ let configFile = '~/.zshrc'
551
+ if (shellType === 'bash') {
552
+ configFile = process.platform === 'darwin' ? '~/.bash_profile' : '~/.bashrc'
553
+ } else if (shellType === 'powershell') {
554
+ configFile = '~/Documents/PowerShell/Microsoft.PowerShell_profile.ps1'
555
+ }
556
+
557
+ console.log(chalk.gray(` source ${configFile}`))
558
+ console.log('')
559
+
560
+ return true
561
+ }
562
+
492
563
  /**
493
564
  * 清空 shell 历史
494
565
  */
@@ -496,7 +567,249 @@ export function clearShellHistory(): void {
496
567
  if (fs.existsSync(SHELL_HISTORY_FILE)) {
497
568
  fs.unlinkSync(SHELL_HISTORY_FILE)
498
569
  }
570
+ const colors = getColors()
499
571
  console.log('')
500
- console.log(chalk.green('✓ Shell 历史已清空'))
572
+ console.log(chalk.hex(colors.success)('✓ Shell 历史已清空'))
501
573
  console.log('')
502
574
  }
575
+
576
+ // ================== 远程 Shell Hook ==================
577
+
578
+ /**
579
+ * 生成远程 zsh hook 脚本
580
+ */
581
+ function generateRemoteZshHook(): string {
582
+ const config = getConfig()
583
+ const limit = config.shellHistoryLimit || 10 // 从配置读取
584
+
585
+ return `
586
+ ${HOOK_START_MARKER}
587
+ # 记录命令到 pretty-please 历史
588
+ __pls_preexec() {
589
+ __PLS_LAST_CMD="$1"
590
+ __PLS_CMD_START=$(date +%s)
591
+ }
592
+
593
+ __pls_precmd() {
594
+ local exit_code=$?
595
+ if [[ -n "$__PLS_LAST_CMD" ]]; then
596
+ local end_time=$(date +%s)
597
+ local timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
598
+ # 确保目录存在
599
+ mkdir -p ~/.please
600
+ # 转义命令中的特殊字符
601
+ local escaped_cmd=$(echo "$__PLS_LAST_CMD" | sed 's/\\\\/\\\\\\\\/g; s/"/\\\\"/g')
602
+ echo "{\\"cmd\\":\\"$escaped_cmd\\",\\"exit\\":$exit_code,\\"time\\":\\"$timestamp\\"}" >> ~/.please/shell_history.jsonl
603
+ # 保持文件不超过 ${limit} 行(从配置读取)
604
+ tail -n ${limit} ~/.please/shell_history.jsonl > ~/.please/shell_history.jsonl.tmp && mv ~/.please/shell_history.jsonl.tmp ~/.please/shell_history.jsonl
605
+ unset __PLS_LAST_CMD
606
+ fi
607
+ }
608
+
609
+ autoload -Uz add-zsh-hook
610
+ add-zsh-hook preexec __pls_preexec
611
+ add-zsh-hook precmd __pls_precmd
612
+ ${HOOK_END_MARKER}
613
+ `
614
+ }
615
+
616
+ /**
617
+ * 生成远程 bash hook 脚本
618
+ */
619
+ function generateRemoteBashHook(): string {
620
+ const config = getConfig()
621
+ const limit = config.shellHistoryLimit || 10 // 从配置读取
622
+
623
+ return `
624
+ ${HOOK_START_MARKER}
625
+ # 记录命令到 pretty-please 历史
626
+ __pls_prompt_command() {
627
+ local exit_code=$?
628
+ local last_cmd=$(history 1 | sed 's/^ *[0-9]* *//')
629
+ if [[ -n "$last_cmd" && "$last_cmd" != "$__PLS_LAST_CMD" ]]; then
630
+ __PLS_LAST_CMD="$last_cmd"
631
+ local timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
632
+ # 确保目录存在
633
+ mkdir -p ~/.please
634
+ local escaped_cmd=$(echo "$last_cmd" | sed 's/\\\\/\\\\\\\\/g; s/"/\\\\"/g')
635
+ echo "{\\"cmd\\":\\"$escaped_cmd\\",\\"exit\\":$exit_code,\\"time\\":\\"$timestamp\\"}" >> ~/.please/shell_history.jsonl
636
+ tail -n ${limit} ~/.please/shell_history.jsonl > ~/.please/shell_history.jsonl.tmp && mv ~/.please/shell_history.jsonl.tmp ~/.please/shell_history.jsonl
637
+ fi
638
+ }
639
+
640
+ if [[ ! "$PROMPT_COMMAND" =~ __pls_prompt_command ]]; then
641
+ PROMPT_COMMAND="__pls_prompt_command;\${PROMPT_COMMAND}"
642
+ fi
643
+ ${HOOK_END_MARKER}
644
+ `
645
+ }
646
+
647
+ /**
648
+ * 检测远程服务器的 shell 类型
649
+ */
650
+ export async function detectRemoteShell(sshExecFn: (cmd: string) => Promise<{ stdout: string; exitCode: number }>): Promise<ShellType> {
651
+ try {
652
+ const result = await sshExecFn('basename "$SHELL"')
653
+ if (result.exitCode === 0) {
654
+ const shell = result.stdout.trim()
655
+ if (shell === 'zsh') return 'zsh'
656
+ if (shell === 'bash') return 'bash'
657
+ }
658
+ } catch {
659
+ // 忽略错误
660
+ }
661
+ return 'bash' // 默认 bash
662
+ }
663
+
664
+ /**
665
+ * 获取远程 shell 配置文件路径
666
+ */
667
+ export function getRemoteShellConfigPath(shellType: ShellType): string {
668
+ switch (shellType) {
669
+ case 'zsh':
670
+ return '~/.zshrc'
671
+ case 'bash':
672
+ return '~/.bashrc'
673
+ default:
674
+ return '~/.bashrc'
675
+ }
676
+ }
677
+
678
+ /**
679
+ * 生成远程 hook 脚本
680
+ */
681
+ export function generateRemoteHookScript(shellType: ShellType): string | null {
682
+ switch (shellType) {
683
+ case 'zsh':
684
+ return generateRemoteZshHook()
685
+ case 'bash':
686
+ return generateRemoteBashHook()
687
+ default:
688
+ return null
689
+ }
690
+ }
691
+
692
+ /**
693
+ * 检查远程 hook 是否已安装
694
+ */
695
+ export async function checkRemoteHookInstalled(
696
+ sshExecFn: (cmd: string) => Promise<{ stdout: string; exitCode: number }>,
697
+ configPath: string
698
+ ): Promise<boolean> {
699
+ try {
700
+ const result = await sshExecFn(`grep -q "${HOOK_START_MARKER}" ${configPath} 2>/dev/null && echo "installed" || echo "not_installed"`)
701
+ return result.stdout.trim() === 'installed'
702
+ } catch {
703
+ return false
704
+ }
705
+ }
706
+
707
+ /**
708
+ * 在远程服务器安装 shell hook
709
+ */
710
+ export async function installRemoteShellHook(
711
+ sshExecFn: (cmd: string) => Promise<{ stdout: string; exitCode: number }>,
712
+ shellType: ShellType
713
+ ): Promise<{ success: boolean; message: string }> {
714
+ const colors = getColors()
715
+ const configPath = getRemoteShellConfigPath(shellType)
716
+ const hookScript = generateRemoteHookScript(shellType)
717
+
718
+ if (!hookScript) {
719
+ return { success: false, message: chalk.hex(colors.error)(`不支持的 shell 类型: ${shellType}`) }
720
+ }
721
+
722
+ // 检查是否已安装
723
+ const installed = await checkRemoteHookInstalled(sshExecFn, configPath)
724
+ if (installed) {
725
+ return { success: true, message: chalk.hex(colors.warning)('Shell hook 已安装,跳过') }
726
+ }
727
+
728
+ // 备份原配置文件
729
+ try {
730
+ await sshExecFn(`cp ${configPath} ${configPath}.pls-backup 2>/dev/null || true`)
731
+ } catch {
732
+ // 忽略备份错误
733
+ }
734
+
735
+ // 安装 hook
736
+ // 使用 cat 和 heredoc 来追加内容
737
+ const escapedScript = hookScript.replace(/'/g, "'\"'\"'")
738
+ const installCmd = `echo '${escapedScript}' >> ${configPath}`
739
+
740
+ try {
741
+ const result = await sshExecFn(installCmd)
742
+ if (result.exitCode !== 0) {
743
+ return { success: false, message: chalk.hex(colors.error)(`安装失败: ${result.stdout}`) }
744
+ }
745
+
746
+ // 确保 ~/.please 目录存在
747
+ await sshExecFn('mkdir -p ~/.please')
748
+
749
+ return {
750
+ success: true,
751
+ message: chalk.hex(colors.success)(`Shell hook 已安装到 ${configPath}`),
752
+ }
753
+ } catch (error) {
754
+ const message = error instanceof Error ? error.message : String(error)
755
+ return { success: false, message: chalk.hex(colors.error)(`安装失败: ${message}`) }
756
+ }
757
+ }
758
+
759
+ /**
760
+ * 从远程服务器卸载 shell hook
761
+ */
762
+ export async function uninstallRemoteShellHook(
763
+ sshExecFn: (cmd: string) => Promise<{ stdout: string; exitCode: number }>,
764
+ shellType: ShellType
765
+ ): Promise<{ success: boolean; message: string }> {
766
+ const colors = getColors()
767
+ const configPath = getRemoteShellConfigPath(shellType)
768
+
769
+ // 检查是否已安装
770
+ const installed = await checkRemoteHookInstalled(sshExecFn, configPath)
771
+ if (!installed) {
772
+ return { success: true, message: chalk.hex(colors.warning)('Shell hook 未安装,跳过') }
773
+ }
774
+
775
+ // 使用 sed 删除 hook 代码块
776
+ // 注意:需要处理特殊字符
777
+ const startMarkerEscaped = HOOK_START_MARKER.replace(/[[\]]/g, '\\$&')
778
+ const endMarkerEscaped = HOOK_END_MARKER.replace(/[[\]]/g, '\\$&')
779
+
780
+ // 在 macOS 和 Linux 上 sed -i 行为不同,使用 sed + 临时文件
781
+ const uninstallCmd = `
782
+ sed '/${startMarkerEscaped}/,/${endMarkerEscaped}/d' ${configPath} > ${configPath}.tmp && mv ${configPath}.tmp ${configPath}
783
+ `
784
+
785
+ try {
786
+ const result = await sshExecFn(uninstallCmd)
787
+ if (result.exitCode !== 0) {
788
+ return { success: false, message: chalk.hex(colors.error)(`卸载失败: ${result.stdout}`) }
789
+ }
790
+
791
+ return {
792
+ success: true,
793
+ message: chalk.hex(colors.success)('Shell hook 已卸载'),
794
+ }
795
+ } catch (error) {
796
+ const message = error instanceof Error ? error.message : String(error)
797
+ return { success: false, message: chalk.hex(colors.error)(`卸载失败: ${message}`) }
798
+ }
799
+ }
800
+
801
+ /**
802
+ * 获取远程 hook 状态
803
+ */
804
+ export async function getRemoteHookStatus(
805
+ sshExecFn: (cmd: string) => Promise<{ stdout: string; exitCode: number }>
806
+ ): Promise<{ installed: boolean; shellType: ShellType; configPath: string }> {
807
+ // 检测 shell 类型
808
+ const shellType = await detectRemoteShell(sshExecFn)
809
+ const configPath = getRemoteShellConfigPath(shellType)
810
+
811
+ // 检查是否已安装
812
+ const installed = await checkRemoteHookInstalled(sshExecFn, configPath)
813
+
814
+ return { installed, shellType, configPath }
815
+ }