@yivan-lab/pretty-please 1.4.0 → 1.5.1

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 (94) hide show
  1. package/README.md +32 -4
  2. package/bin/pls.tsx +153 -35
  3. package/dist/bin/pls.js +126 -23
  4. package/dist/package.json +10 -2
  5. package/dist/src/__integration__/command-generation.test.d.ts +5 -0
  6. package/dist/src/__integration__/command-generation.test.js +508 -0
  7. package/dist/src/__integration__/error-recovery.test.d.ts +5 -0
  8. package/dist/src/__integration__/error-recovery.test.js +511 -0
  9. package/dist/src/__integration__/shell-hook-workflow.test.d.ts +5 -0
  10. package/dist/src/__integration__/shell-hook-workflow.test.js +375 -0
  11. package/dist/src/__tests__/alias.test.d.ts +5 -0
  12. package/dist/src/__tests__/alias.test.js +421 -0
  13. package/dist/src/__tests__/chat-history.test.d.ts +5 -0
  14. package/dist/src/__tests__/chat-history.test.js +372 -0
  15. package/dist/src/__tests__/config.test.d.ts +5 -0
  16. package/dist/src/__tests__/config.test.js +822 -0
  17. package/dist/src/__tests__/history.test.d.ts +5 -0
  18. package/dist/src/__tests__/history.test.js +439 -0
  19. package/dist/src/__tests__/remote-history.test.d.ts +5 -0
  20. package/dist/src/__tests__/remote-history.test.js +641 -0
  21. package/dist/src/__tests__/remote.test.d.ts +5 -0
  22. package/dist/src/__tests__/remote.test.js +689 -0
  23. package/dist/src/__tests__/shell-hook-install.test.d.ts +5 -0
  24. package/dist/src/__tests__/shell-hook-install.test.js +413 -0
  25. package/dist/src/__tests__/shell-hook-remote.test.d.ts +5 -0
  26. package/dist/src/__tests__/shell-hook-remote.test.js +507 -0
  27. package/dist/src/__tests__/shell-hook.test.d.ts +5 -0
  28. package/dist/src/__tests__/shell-hook.test.js +440 -0
  29. package/dist/src/__tests__/sysinfo.test.d.ts +5 -0
  30. package/dist/src/__tests__/sysinfo.test.js +572 -0
  31. package/dist/src/__tests__/system-history.test.d.ts +5 -0
  32. package/dist/src/__tests__/system-history.test.js +457 -0
  33. package/dist/src/components/Chat.js +9 -28
  34. package/dist/src/config.d.ts +2 -0
  35. package/dist/src/config.js +30 -2
  36. package/dist/src/mastra-chat.js +6 -3
  37. package/dist/src/multi-step.js +6 -3
  38. package/dist/src/project-context.d.ts +22 -0
  39. package/dist/src/project-context.js +168 -0
  40. package/dist/src/prompts.d.ts +4 -4
  41. package/dist/src/prompts.js +23 -6
  42. package/dist/src/shell-hook.d.ts +13 -0
  43. package/dist/src/shell-hook.js +163 -33
  44. package/dist/src/sysinfo.d.ts +38 -9
  45. package/dist/src/sysinfo.js +245 -21
  46. package/dist/src/system-history.d.ts +5 -0
  47. package/dist/src/system-history.js +64 -18
  48. package/dist/src/ui/__tests__/theme.test.d.ts +5 -0
  49. package/dist/src/ui/__tests__/theme.test.js +688 -0
  50. package/dist/src/upgrade.js +3 -0
  51. package/dist/src/user-preferences.d.ts +44 -0
  52. package/dist/src/user-preferences.js +147 -0
  53. package/dist/src/utils/__tests__/platform-capabilities.test.d.ts +5 -0
  54. package/dist/src/utils/__tests__/platform-capabilities.test.js +214 -0
  55. package/dist/src/utils/__tests__/platform-exec.test.d.ts +5 -0
  56. package/dist/src/utils/__tests__/platform-exec.test.js +212 -0
  57. package/dist/src/utils/__tests__/platform-shell.test.d.ts +5 -0
  58. package/dist/src/utils/__tests__/platform-shell.test.js +300 -0
  59. package/dist/src/utils/__tests__/platform.test.d.ts +5 -0
  60. package/dist/src/utils/__tests__/platform.test.js +137 -0
  61. package/dist/src/utils/platform.d.ts +88 -0
  62. package/dist/src/utils/platform.js +331 -0
  63. package/package.json +10 -2
  64. package/src/__integration__/command-generation.test.ts +602 -0
  65. package/src/__integration__/error-recovery.test.ts +620 -0
  66. package/src/__integration__/shell-hook-workflow.test.ts +457 -0
  67. package/src/__tests__/alias.test.ts +545 -0
  68. package/src/__tests__/chat-history.test.ts +462 -0
  69. package/src/__tests__/config.test.ts +1043 -0
  70. package/src/__tests__/history.test.ts +538 -0
  71. package/src/__tests__/remote-history.test.ts +791 -0
  72. package/src/__tests__/remote.test.ts +866 -0
  73. package/src/__tests__/shell-hook-install.test.ts +510 -0
  74. package/src/__tests__/shell-hook-remote.test.ts +679 -0
  75. package/src/__tests__/shell-hook.test.ts +564 -0
  76. package/src/__tests__/sysinfo.test.ts +718 -0
  77. package/src/__tests__/system-history.test.ts +608 -0
  78. package/src/components/Chat.tsx +10 -37
  79. package/src/config.ts +29 -2
  80. package/src/mastra-chat.ts +8 -3
  81. package/src/multi-step.ts +7 -2
  82. package/src/project-context.ts +191 -0
  83. package/src/prompts.ts +26 -5
  84. package/src/shell-hook.ts +179 -33
  85. package/src/sysinfo.ts +326 -25
  86. package/src/system-history.ts +67 -14
  87. package/src/ui/__tests__/theme.test.ts +869 -0
  88. package/src/upgrade.ts +5 -0
  89. package/src/user-preferences.ts +178 -0
  90. package/src/utils/__tests__/platform-capabilities.test.ts +265 -0
  91. package/src/utils/__tests__/platform-exec.test.ts +278 -0
  92. package/src/utils/__tests__/platform-shell.test.ts +353 -0
  93. package/src/utils/__tests__/platform.test.ts +170 -0
  94. package/src/utils/platform.ts +431 -0
package/src/shell-hook.ts CHANGED
@@ -5,6 +5,10 @@ import chalk from 'chalk'
5
5
  import { CONFIG_DIR, getConfig, setConfigValue } from './config.js'
6
6
  import { getHistory } from './history.js'
7
7
  import { getCurrentTheme } from './ui/theme.js'
8
+ import {
9
+ detectShell as platformDetectShell,
10
+ type ShellType as PlatformShellType,
11
+ } from './utils/platform.js'
8
12
 
9
13
  // 获取主题颜色
10
14
  function getColors() {
@@ -24,9 +28,29 @@ const SHELL_HISTORY_FILE = path.join(CONFIG_DIR, 'shell_history.jsonl')
24
28
  const HOOK_START_MARKER = '# >>> pretty-please shell hook >>>'
25
29
  const HOOK_END_MARKER = '# <<< pretty-please shell hook <<<'
26
30
 
27
- // Shell 类型
31
+ // Shell 类型(保持向后兼容,但内部使用更细分的类型)
28
32
  type ShellType = 'zsh' | 'bash' | 'powershell' | 'unknown'
29
33
 
34
+ /**
35
+ * 将 platform 模块的 ShellType 转换为本地 ShellType
36
+ */
37
+ function toLocalShellType(platformShell: PlatformShellType): ShellType {
38
+ switch (platformShell) {
39
+ case 'zsh':
40
+ return 'zsh'
41
+ case 'bash':
42
+ return 'bash'
43
+ case 'powershell5':
44
+ case 'powershell7':
45
+ return 'powershell'
46
+ case 'cmd':
47
+ case 'fish':
48
+ case 'unknown':
49
+ default:
50
+ return 'unknown'
51
+ }
52
+ }
53
+
30
54
  /**
31
55
  * Shell 历史记录项
32
56
  */
@@ -49,14 +73,17 @@ export interface HookStatus {
49
73
 
50
74
  /**
51
75
  * 检测当前 shell 类型
76
+ * 使用 platform 模块进行跨平台检测
52
77
  */
53
78
  export function detectShell(): ShellType {
54
- const shell = process.env.SHELL || ''
55
- if (shell.includes('zsh')) return 'zsh'
56
- if (shell.includes('bash')) return 'bash'
57
- // Windows PowerShell
58
- if (process.platform === 'win32') return 'powershell'
59
- return 'unknown'
79
+ const platformShell = platformDetectShell()
80
+
81
+ // CMD 不支持 Hook,提示用户
82
+ if (platformShell === 'cmd') {
83
+ return 'unknown'
84
+ }
85
+
86
+ return toLocalShellType(platformShell)
60
87
  }
61
88
 
62
89
  /**
@@ -81,6 +108,36 @@ export function getShellConfigPath(shellType: ShellType): string | null {
81
108
  }
82
109
  }
83
110
 
111
+ /**
112
+ * 生成命令统计的公共 shell 函数
113
+ * 用于 zsh 和 bash,避免代码重复
114
+ */
115
+ function generateStatFunction(): string {
116
+ return `
117
+ # 统计命令频率(公共函数)
118
+ __pls_record_stat() {
119
+ local cmd_name="$1"
120
+ local stats_file="${CONFIG_DIR}/command_stats.txt"
121
+
122
+ # 确保文件存在
123
+ touch "$stats_file"
124
+
125
+ # 更新统计(纯 shell 实现,不依赖 jq)
126
+ if grep -q "^$cmd_name=" "$stats_file" 2>/dev/null; then
127
+ # 命令已存在,次数 +1
128
+ local count=$(grep "^$cmd_name=" "$stats_file" | cut -d= -f2)
129
+ count=$((count + 1))
130
+ # macOS 和 Linux 的 sed -i 不同,使用临时文件
131
+ sed "s/^$cmd_name=.*/$cmd_name=$count/" "$stats_file" > "$stats_file.tmp" 2>/dev/null
132
+ mv "$stats_file.tmp" "$stats_file" 2>/dev/null
133
+ else
134
+ # 新命令,追加
135
+ echo "$cmd_name=1" >> "$stats_file"
136
+ fi
137
+ }
138
+ `
139
+ }
140
+
84
141
  /**
85
142
  * 生成 zsh hook 脚本
86
143
  */
@@ -91,6 +148,7 @@ function generateZshHook(): string {
91
148
  return `
92
149
  ${HOOK_START_MARKER}
93
150
  # 记录命令到 pretty-please 历史
151
+ ${generateStatFunction()}
94
152
  __pls_preexec() {
95
153
  __PLS_LAST_CMD="$1"
96
154
  __PLS_CMD_START=$(date +%s)
@@ -106,6 +164,11 @@ __pls_precmd() {
106
164
  echo "{\\"cmd\\":\\"$escaped_cmd\\",\\"exit\\":$exit_code,\\"time\\":\\"$timestamp\\"}" >> "${CONFIG_DIR}/shell_history.jsonl"
107
165
  # 保持文件不超过 ${limit} 行(从配置读取)
108
166
  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"
167
+
168
+ # 统计命令频率
169
+ local cmd_name=$(echo "$__PLS_LAST_CMD" | awk '{print $1}')
170
+ __pls_record_stat "$cmd_name"
171
+
109
172
  unset __PLS_LAST_CMD
110
173
  fi
111
174
  }
@@ -127,6 +190,7 @@ function generateBashHook(): string {
127
190
  return `
128
191
  ${HOOK_START_MARKER}
129
192
  # 记录命令到 pretty-please 历史
193
+ ${generateStatFunction()}
130
194
  __pls_prompt_command() {
131
195
  local exit_code=$?
132
196
  local last_cmd=$(history 1 | sed 's/^ *[0-9]* *//')
@@ -136,6 +200,10 @@ __pls_prompt_command() {
136
200
  local escaped_cmd=$(echo "$last_cmd" | sed 's/\\\\/\\\\\\\\/g; s/"/\\\\"/g')
137
201
  echo "{\\"cmd\\":\\"$escaped_cmd\\",\\"exit\\":$exit_code,\\"time\\":\\"$timestamp\\"}" >> "${CONFIG_DIR}/shell_history.jsonl"
138
202
  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"
203
+
204
+ # 统计命令频率
205
+ local cmd_name=$(echo "$last_cmd" | awk '{print $1}')
206
+ __pls_record_stat "$cmd_name"
139
207
  fi
140
208
  }
141
209
 
@@ -148,16 +216,27 @@ ${HOOK_END_MARKER}
148
216
 
149
217
  /**
150
218
  * 生成 PowerShell hook 脚本
219
+ * 使用 PowerShell 原生路径处理,避免跨平台路径问题
151
220
  */
152
221
  function generatePowerShellHook(): string {
153
222
  const config = getConfig()
154
223
  const limit = config.shellHistoryLimit || 10 // 从配置读取
155
224
 
225
+ // 使用 PowerShell 原生路径变量,而不是嵌入 Node.js 路径
156
226
  return `
157
227
  ${HOOK_START_MARKER}
158
228
  # 记录命令到 pretty-please 历史
229
+ # 使用 PowerShell 原生路径
230
+ $Global:__PlsDir = Join-Path $env:USERPROFILE ".please"
231
+ $Global:__PlsHistoryFile = Join-Path $Global:__PlsDir "shell_history.jsonl"
232
+ $Global:__PlsStatsFile = Join-Path $Global:__PlsDir "command_stats.txt"
159
233
  $Global:__PlsLastCmd = ""
160
234
 
235
+ # 确保目录存在
236
+ if (-not (Test-Path $Global:__PlsDir)) {
237
+ New-Item -Path $Global:__PlsDir -ItemType Directory -Force | Out-Null
238
+ }
239
+
161
240
  function __Pls_RecordCommand {
162
241
  $lastCmd = (Get-History -Count 1).CommandLine
163
242
  if ($lastCmd -and $lastCmd -ne $Global:__PlsLastCmd) {
@@ -167,10 +246,38 @@ function __Pls_RecordCommand {
167
246
  $timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
168
247
  $escapedCmd = $lastCmd -replace '\\\\', '\\\\\\\\' -replace '"', '\\\\"'
169
248
  $json = "{\`"cmd\`":\`"$escapedCmd\`",\`"exit\`":$exitCode,\`"time\`":\`"$timestamp\`"}"
170
- Add-Content -Path "${CONFIG_DIR}/shell_history.jsonl" -Value $json
249
+ Add-Content -Path $Global:__PlsHistoryFile -Value $json
171
250
  # 保持文件不超过 ${limit} 行(从配置读取)
172
- $content = Get-Content "${CONFIG_DIR}/shell_history.jsonl" -Tail ${limit}
173
- $content | Set-Content "${CONFIG_DIR}/shell_history.jsonl"
251
+ $content = Get-Content $Global:__PlsHistoryFile -Tail ${limit} -ErrorAction SilentlyContinue
252
+ if ($content) {
253
+ $content | Set-Content $Global:__PlsHistoryFile
254
+ }
255
+
256
+ # 统计命令频率
257
+ $cmdName = $lastCmd -split ' ' | Select-Object -First 1
258
+
259
+ # 确保统计文件存在
260
+ if (-not (Test-Path $Global:__PlsStatsFile)) {
261
+ New-Item -Path $Global:__PlsStatsFile -ItemType File -Force | Out-Null
262
+ }
263
+
264
+ # 更新统计
265
+ $stats = Get-Content $Global:__PlsStatsFile -ErrorAction SilentlyContinue
266
+ $found = $false
267
+ $newStats = @()
268
+ foreach ($line in $stats) {
269
+ if ($line -match "^$cmdName=(\\d+)$") {
270
+ $count = [int]$matches[1] + 1
271
+ $newStats += "$cmdName=$count"
272
+ $found = $true
273
+ } else {
274
+ $newStats += $line
275
+ }
276
+ }
277
+ if (-not $found) {
278
+ $newStats += "$cmdName=1"
279
+ }
280
+ $newStats | Set-Content $Global:__PlsStatsFile
174
281
  }
175
282
  }
176
283
 
@@ -211,6 +318,20 @@ export async function installShellHook(): Promise<boolean> {
211
318
 
212
319
  if (!configPath) {
213
320
  console.log(chalk.hex(colors.error)(`❌ 不支持的 shell 类型: ${shellType}`))
321
+
322
+ // CMD 特殊提示
323
+ if (shellType === 'unknown') {
324
+ const platformShell = platformDetectShell()
325
+ if (platformShell === 'cmd') {
326
+ console.log('')
327
+ console.log(chalk.hex(colors.warning)('⚠️ CMD 不支持 Shell Hook 功能'))
328
+ console.log(chalk.hex(colors.secondary)('建议使用 PowerShell 获得完整体验:'))
329
+ console.log(chalk.hex(colors.secondary)(' 1. 按 Win 键搜索 "PowerShell"'))
330
+ console.log(chalk.hex(colors.secondary)(' 2. 在 PowerShell 中运行 pls hook install'))
331
+ console.log('')
332
+ }
333
+ }
334
+
214
335
  return false
215
336
  }
216
337
 
@@ -518,10 +639,17 @@ export function displayShellHistory(): void {
518
639
  }
519
640
 
520
641
  /**
521
- * shellHistoryLimit 变化时,自动重装 Hook
522
- * 返回是否成功重装
642
+ * 重新安装 Shell Hook(通用函数)
643
+ * 用于版本升级、配置变更等场景
644
+ *
645
+ * @param options.silent 是否静默模式(不输出日志)
646
+ * @param options.reason 重装原因(用于日志显示)
647
+ * @returns 是否成功重装
523
648
  */
524
- export async function reinstallHookForLimitChange(oldLimit: number, newLimit: number): Promise<boolean> {
649
+ export async function reinstallShellHook(options?: {
650
+ silent?: boolean
651
+ reason?: string
652
+ }): Promise<boolean> {
525
653
  const config = getConfig()
526
654
 
527
655
  // 只有在 hook 已启用时才重装
@@ -529,37 +657,55 @@ export async function reinstallHookForLimitChange(oldLimit: number, newLimit: nu
529
657
  return false
530
658
  }
531
659
 
532
- // 值没有变化,不需要重装
533
- if (oldLimit === newLimit) {
534
- return false
535
- }
536
-
537
660
  const colors = getColors()
661
+ const { silent = false, reason } = options || {}
538
662
 
539
- console.log('')
540
- console.log(chalk.hex(colors.primary)(`检测到 shellHistoryLimit 变化 (${oldLimit} → ${newLimit})`))
541
- console.log(chalk.hex(colors.primary)('正在更新 Shell Hook...'))
663
+ if (!silent) {
664
+ console.log('')
665
+ if (reason) {
666
+ console.log(chalk.hex(colors.primary)(reason))
667
+ }
668
+ console.log(chalk.hex(colors.primary)('正在更新 Shell Hook...'))
669
+ }
542
670
 
671
+ // 卸载旧版本,安装新版本
543
672
  uninstallShellHook()
544
673
  await installShellHook()
545
674
 
546
- console.log('')
547
- console.log(chalk.hex(colors.warning)('⚠️ 请重启终端或运行以下命令使新配置生效:'))
675
+ if (!silent) {
676
+ console.log('')
677
+ console.log(chalk.hex(colors.warning)('⚠️ 请重启终端或运行以下命令使新配置生效:'))
678
+
679
+ const shellType = detectShell()
680
+ let configFile = '~/.zshrc'
681
+ if (shellType === 'bash') {
682
+ configFile = process.platform === 'darwin' ? '~/.bash_profile' : '~/.bashrc'
683
+ } else if (shellType === 'powershell') {
684
+ configFile = '~/Documents/PowerShell/Microsoft.PowerShell_profile.ps1'
685
+ }
548
686
 
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'
687
+ console.log(chalk.gray(` source ${configFile}`))
688
+ console.log('')
555
689
  }
556
690
 
557
- console.log(chalk.gray(` source ${configFile}`))
558
- console.log('')
559
-
560
691
  return true
561
692
  }
562
693
 
694
+ /**
695
+ * 当 shellHistoryLimit 变化时,自动重装 Hook
696
+ * 返回是否成功重装
697
+ */
698
+ export async function reinstallHookForLimitChange(oldLimit: number, newLimit: number): Promise<boolean> {
699
+ // 值没有变化,不需要重装
700
+ if (oldLimit === newLimit) {
701
+ return false
702
+ }
703
+
704
+ return reinstallShellHook({
705
+ reason: `检测到 shellHistoryLimit 变化 (${oldLimit} → ${newLimit})`,
706
+ })
707
+ }
708
+
563
709
  /**
564
710
  * 清空 shell 历史
565
711
  */
@@ -641,7 +787,7 @@ export function formatShellHistoryForAIWithFallback(): string {
641
787
  }
642
788
 
643
789
  // 格式化系统历史(简单格式,无详细信息)
644
- const lines = history.map((item, index) => {
790
+ const lines = history.map((item: ShellHistoryItem, index: number) => {
645
791
  const status = item.exit === 0 ? '✓' : `✗ 退出码:${item.exit}`
646
792
  return `${index + 1}. ${item.cmd} ${status}`
647
793
  })