@yivan-lab/pretty-please 1.4.0 → 1.5.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 (94) hide show
  1. package/README.md +30 -2
  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/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  > `please 查看当前目录` — 就像在跟电脑说话一样自然
8
8
 
9
9
  <p align="center">
10
- <img src="https://github.com/user-attachments/assets/e06a562c-f899-41d2-8870-d38ebc249904" alt="Pretty Please Demo" width="800">
10
+ <img src="https://github.com/user-attachments/assets/aeebcac4-ee52-4b9d-b2c9-77c2930e7454" alt="Pretty Please Demo" width="800">
11
11
  </p>
12
12
 
13
13
  ## 这是啥?
@@ -54,7 +54,6 @@ Enumerating objects: 5, done.
54
54
  - **命令打错了?** 直接 `pls` 自动修复,像 thefuck 一样方便,但更智能
55
55
  - 记不住 `tar` 的一堆参数
56
56
  - 想批量处理文件但懒得写脚本
57
- - 需要在多台服务器上执行同样的操作
58
57
  - 想问问某个命令怎么用
59
58
 
60
59
  ## 能干啥?
@@ -66,6 +65,7 @@ Enumerating objects: 5, done.
66
65
  - **错误恢复重试** - 命令失败了 AI 会分析原因并调整策略
67
66
 
68
67
  **高级功能:**
68
+ - **学习你的习惯** - 开启 Shell Hook 后,AI 会记住你常用的命令,下次优先用你习惯的工具
69
69
  - **远程执行** - 通过 SSH 在服务器上跑命令,支持批量(`-r server1,server2,server3`)
70
70
  - **对话模式** - `pls chat grep 怎么用`,随时问问题
71
71
  - **命令别名** - 把常用操作存成快捷方式
@@ -74,6 +74,8 @@ Enumerating objects: 5, done.
74
74
 
75
75
  ## 安装
76
76
 
77
+ 注意:目前 pls 在 windows 端可能会有不兼容导致的 bug,如果遇到可以发 issue 反馈,谢谢
78
+
77
79
  **方式一:npm(推荐)**
78
80
 
79
81
  ```bash
@@ -86,8 +88,14 @@ npm i -g @yivan-lab/pretty-please
86
88
  # Linux / macOS
87
89
  curl -fsSL https://raw.githubusercontent.com/IvanLark/pretty-please/main/install.sh | bash
88
90
 
91
+ # Linux / macOS(国内加速)
92
+ curl -fsSL https://gh-proxy.org/https://raw.githubusercontent.com/IvanLark/pretty-please/main/install.sh | bash
93
+
89
94
  # Windows PowerShell
90
95
  irm https://raw.githubusercontent.com/IvanLark/pretty-please/main/install.ps1 | iex
96
+
97
+ # Windows PowerShell(国内加速)
98
+ irm https://gh-proxy.org/https://raw.githubusercontent.com/IvanLark/pretty-please/main/install.ps1 | iex
91
99
  ```
92
100
 
93
101
  支持平台:Linux (x64/arm64) / macOS (Intel/Apple Silicon) / Windows x64
@@ -255,6 +263,22 @@ pls hook uninstall # 卸载 hook
255
263
 
256
264
  支持 zsh / bash / PowerShell。
257
265
 
266
+ **开了 Hook 后,pls 会学习你的命令习惯。** 比如你平时用 `eza` 而不是 `ls`,用 `bat` 而不是 `cat`,AI 生成命令时会优先用你习惯的工具。用得越多,AI 越懂你。
267
+
268
+ ```bash
269
+ pls prefs # 看看 AI 学到了什么
270
+ pls prefs clear # 清空偏好统计
271
+ ```
272
+
273
+ ### 系统信息
274
+
275
+ 查看当前系统信息(AI 生成命令时会参考这些):
276
+
277
+ ```bash
278
+ pls sysinfo # 查看系统信息
279
+ pls sysinfo refresh # 刷新缓存
280
+ ```
281
+
258
282
  ### 主题
259
283
 
260
284
  7 个内置主题 + 自定义主题:
@@ -371,6 +395,10 @@ pls hook install # 安装
371
395
  pls hook status # 状态
372
396
  pls hook uninstall # 卸载
373
397
 
398
+ # 偏好 & 系统
399
+ pls prefs # 查看命令偏好
400
+ pls sysinfo # 查看系统信息
401
+
374
402
  # 主题
375
403
  pls theme # 当前主题
376
404
  pls theme list # 所有主题
package/bin/pls.tsx CHANGED
@@ -3,7 +3,7 @@ import { Command } from 'commander'
3
3
  import { fileURLToPath } from 'url'
4
4
  import { dirname, join } from 'path'
5
5
  import path from 'path'
6
- import { exec } from 'child_process'
6
+ import { exec, spawn } from 'child_process'
7
7
  import fs from 'fs'
8
8
  import os from 'os'
9
9
  import chalk from 'chalk'
@@ -12,7 +12,7 @@ import chalk from 'chalk'
12
12
  // import { render } from 'ink'
13
13
  // import { MultiStepCommandGenerator } from '../src/components/MultiStepCommandGenerator.js'
14
14
  // import { Chat } from '../src/components/Chat.js'
15
- import { isConfigValid, setConfigValue, getConfig, maskApiKey } from '../src/config.js'
15
+ import { isConfigValid, setConfigValue, getConfig, maskApiKey, displayConfig } from '../src/config.js'
16
16
  import { clearHistory, addHistory, getHistory, getHistoryFilePath } from '../src/history.js'
17
17
  import { clearChatHistory, getChatRoundCount, getChatHistoryFilePath, displayChatHistory } from '../src/chat-history.js'
18
18
  import { type ExecutedStep } from '../src/multi-step.js'
@@ -50,6 +50,12 @@ import {
50
50
  generateBatchRemoteCommands,
51
51
  executeBatchRemoteCommands,
52
52
  } from '../src/remote.js'
53
+ import { getSystemInfo, formatSystemInfo, refreshSystemCache, displaySystemInfo } from '../src/sysinfo.js'
54
+ import {
55
+ displayCommandStats,
56
+ clearCommandStats,
57
+ getStatsFilePath,
58
+ } from '../src/user-preferences.js'
53
59
  import {
54
60
  addRemoteHistory,
55
61
  displayRemoteHistory,
@@ -65,6 +71,11 @@ import {
65
71
  uninstallRemoteShellHook,
66
72
  getRemoteHookStatus,
67
73
  } from '../src/shell-hook.js'
74
+ import {
75
+ buildShellExecConfig,
76
+ getDefaultShell,
77
+ isWindows,
78
+ } from '../src/utils/platform.js'
68
79
 
69
80
  // 获取主题颜色的辅助函数
70
81
  function getThemeColors() {
@@ -116,10 +127,92 @@ process.on('beforeExit', () => {
116
127
  }
117
128
  })
118
129
 
130
+ /**
131
+ * 需要 TTY 的工具白名单
132
+ * 这些工具在 pipe 模式下可能会卡住或无输出
133
+ */
134
+ const TTY_REQUIRED_COMMANDS = new Set([
135
+ // ls 替代品(带图标/颜色)
136
+ 'eza', 'exa', 'lsd',
137
+ // cat 替代品(带语法高亮)
138
+ 'bat', 'batcat',
139
+ // diff 替代品
140
+ 'delta', 'diff-so-fancy',
141
+ // 系统监控
142
+ 'htop', 'btop', 'top', 'glances', 'gtop', 'bpytop',
143
+ // 编辑器
144
+ 'vim', 'nvim', 'nano', 'emacs', 'micro', 'helix', 'hx',
145
+ // 分页器
146
+ 'less', 'more', 'most',
147
+ // 模糊搜索
148
+ 'fzf', 'skim', 'sk',
149
+ // 终端复用器
150
+ 'tmux', 'screen', 'zellij',
151
+ // TUI 工具
152
+ 'lazygit', 'lazydocker', 'lazysql', 'k9s', 'tig',
153
+ // 文件管理器
154
+ 'nnn', 'ranger', 'lf', 'yazi', 'mc', 'vifm',
155
+ // 数据查看
156
+ 'visidata', 'vd',
157
+ ])
158
+
159
+ /**
160
+ * 使用 inherit 模式执行命令(用于需要 TTY 的工具)
161
+ * 特点:命令能正常执行,但无法捕获输出
162
+ */
163
+ function executeWithInherit(command: string): Promise<{ exitCode: number; output: string; stdout: string; stderr: string }> {
164
+ return new Promise((resolve) => {
165
+ console.log('') // 空行
166
+
167
+ // 计算命令框宽度
168
+ const termWidth = process.stdout.columns || 80
169
+ const maxContentWidth = termWidth - 6
170
+ const lines = command.split('\n')
171
+ const wrappedLines: string[] = []
172
+ for (const line of lines) {
173
+ wrappedLines.push(...console2.wrapText(line, maxContentWidth))
174
+ }
175
+ const actualMaxWidth = Math.max(
176
+ ...wrappedLines.map((l) => console2.getDisplayWidth(l)),
177
+ console2.getDisplayWidth('生成命令')
178
+ )
179
+ const boxWidth = Math.max(console2.MIN_COMMAND_BOX_WIDTH, Math.min(actualMaxWidth + 4, termWidth - 2))
180
+ console2.printSeparator('输出', boxWidth)
181
+
182
+ // 使用 platform 模块构建跨平台命令执行配置
183
+ const execConfig = buildShellExecConfig(command)
184
+
185
+ // 使用 spawn + inherit(输出直接到终端)
186
+ const child = spawn(execConfig.shell, execConfig.args, {
187
+ stdio: 'inherit',
188
+ env: process.env,
189
+ })
190
+
191
+ child.on('close', (code) => {
192
+ console2.printSeparator('', boxWidth)
193
+ resolve({ exitCode: code || 0, output: '', stdout: '', stderr: '' })
194
+ })
195
+
196
+ child.on('error', (err) => {
197
+ console2.printSeparator('', boxWidth)
198
+ console2.error(err.message)
199
+ resolve({ exitCode: 1, output: err.message, stdout: '', stderr: err.message })
200
+ })
201
+ })
202
+ }
203
+
119
204
  /**
120
205
  * 执行命令(原生版本)
121
206
  */
122
207
  function executeCommand(command: string): Promise<{ exitCode: number; output: string; stdout: string; stderr: string }> {
208
+ // 检测是否是需要 TTY 的工具
209
+ const firstCmd = command.trim().split(/[\s|&;]/)[0]
210
+ if (TTY_REQUIRED_COMMANDS.has(firstCmd)) {
211
+ // 使用 inherit 模式执行(无法捕获输出,但能正常运行)
212
+ return executeWithInherit(command)
213
+ }
214
+
215
+ // 普通命令:使用 pipe 模式(捕获输出)
123
216
  return new Promise((resolve) => {
124
217
  let stdout = ''
125
218
  let stderr = ''
@@ -142,8 +235,10 @@ function executeCommand(command: string): Promise<{ exitCode: number; output: st
142
235
  const boxWidth = Math.max(console2.MIN_COMMAND_BOX_WIDTH, Math.min(actualMaxWidth + 4, termWidth - 2))
143
236
  console2.printSeparator('输出', boxWidth)
144
237
 
145
- // 使用 bash 并启用 pipefail,确保管道中任何命令失败都能正确返回非零退出码
146
- const child = exec(`set -o pipefail; ${command}`, { shell: '/bin/bash' })
238
+ // 使用 platform 模块构建跨平台命令执行配置
239
+ const execConfig = buildShellExecConfig(command)
240
+
241
+ const child = exec(execConfig.command, { shell: execConfig.shell })
147
242
 
148
243
  child.stdout?.on('data', (data) => {
149
244
  stdout += data
@@ -191,37 +286,7 @@ configCmd
191
286
  .alias('show')
192
287
  .description('查看当前配置')
193
288
  .action(() => {
194
- const config = getConfig()
195
- const CONFIG_FILE = join(os.homedir(), '.please', 'config.json')
196
-
197
- console.log('')
198
- console2.title('当前配置:')
199
- console2.muted('━'.repeat(50))
200
- console.log(` ${chalk.hex(getThemeColors().primary)('apiKey')}: ${maskApiKey(config.apiKey)}`)
201
- console.log(` ${chalk.hex(getThemeColors().primary)('baseUrl')}: ${config.baseUrl}`)
202
- console.log(` ${chalk.hex(getThemeColors().primary)('provider')}: ${config.provider}`)
203
- console.log(` ${chalk.hex(getThemeColors().primary)('model')}: ${config.model}`)
204
- console.log(
205
- ` ${chalk.hex(getThemeColors().primary)('shellHook')}: ${
206
- config.shellHook ? chalk.hex(getThemeColors().success)('已启用') : chalk.gray('未启用')
207
- }`
208
- )
209
- console.log(
210
- ` ${chalk.hex(getThemeColors().primary)('editMode')}: ${
211
- config.editMode === 'auto' ? chalk.hex(getThemeColors().primary)('auto (自动编辑)') : chalk.gray('manual (按E编辑)')
212
- }`
213
- )
214
- console.log(` ${chalk.hex(getThemeColors().primary)('chatHistoryLimit')}: ${config.chatHistoryLimit} 轮`)
215
- console.log(` ${chalk.hex(getThemeColors().primary)('commandHistoryLimit')}: ${config.commandHistoryLimit} 条`)
216
- console.log(` ${chalk.hex(getThemeColors().primary)('shellHistoryLimit')}: ${config.shellHistoryLimit} 条`)
217
- console.log(
218
- ` ${chalk.hex(getThemeColors().primary)('theme')}: ${
219
- config.theme === 'dark' ? chalk.hex(getThemeColors().primary)('dark (深色)') : chalk.hex(getThemeColors().primary)('light (浅色)')
220
- }`
221
- )
222
- console2.muted('━'.repeat(50))
223
- console2.muted(`配置文件: ${CONFIG_FILE}`)
224
- console.log('')
289
+ displayConfig()
225
290
  })
226
291
 
227
292
  configCmd
@@ -816,6 +881,59 @@ aliasCmd.action(() => {
816
881
  displayAliases()
817
882
  })
818
883
 
884
+ // sysinfo 子命令
885
+ const sysinfoCmd = program.command('sysinfo').description('管理系统信息')
886
+
887
+ sysinfoCmd
888
+ .command('show')
889
+ .description('查看系统信息')
890
+ .action(async () => {
891
+ const info = await getSystemInfo()
892
+ displaySystemInfo(info)
893
+ })
894
+
895
+ sysinfoCmd
896
+ .command('refresh')
897
+ .description('刷新系统信息缓存')
898
+ .action(() => {
899
+ console.log('')
900
+ refreshSystemCache()
901
+ console.log('')
902
+ })
903
+
904
+ // 默认 sysinfo 命令(显示信息)
905
+ sysinfoCmd.action(async () => {
906
+ const info = await getSystemInfo()
907
+ displaySystemInfo(info)
908
+ })
909
+
910
+ // prefs 子命令
911
+ const prefsCmd = program.command('prefs').description('管理命令偏好统计')
912
+
913
+ prefsCmd
914
+ .command('show')
915
+ .description('查看命令偏好统计')
916
+ .action(() => {
917
+ displayCommandStats()
918
+ })
919
+
920
+ prefsCmd
921
+ .command('clear')
922
+ .description('清空偏好统计')
923
+ .action(() => {
924
+ const colors = getThemeColors()
925
+ clearCommandStats()
926
+ console.log('')
927
+ console.log(chalk.hex(colors.success)('✓ 已清空命令偏好统计'))
928
+ console.log(chalk.gray(` 统计文件: ${getStatsFilePath()}`))
929
+ console.log('')
930
+ })
931
+
932
+ // 默认 prefs 命令(显示统计)
933
+ prefsCmd.action(() => {
934
+ displayCommandStats()
935
+ })
936
+
819
937
  // remote 子命令
820
938
  const remoteCmd = program.command('remote').description('管理远程服务器')
821
939
 
package/dist/bin/pls.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
3
  import { fileURLToPath } from 'url';
4
- import { dirname, join } from 'path';
4
+ import { dirname } from 'path';
5
5
  import path from 'path';
6
- import { exec } from 'child_process';
6
+ import { exec, spawn } from 'child_process';
7
7
  import fs from 'fs';
8
8
  import os from 'os';
9
9
  import chalk from 'chalk';
@@ -12,7 +12,7 @@ import chalk from 'chalk';
12
12
  // import { render } from 'ink'
13
13
  // import { MultiStepCommandGenerator } from '../src/components/MultiStepCommandGenerator.js'
14
14
  // import { Chat } from '../src/components/Chat.js'
15
- import { isConfigValid, setConfigValue, getConfig, maskApiKey } from '../src/config.js';
15
+ import { isConfigValid, setConfigValue, getConfig, displayConfig } from '../src/config.js';
16
16
  import { clearHistory, addHistory, getHistory, getHistoryFilePath } from '../src/history.js';
17
17
  import { clearChatHistory, getChatRoundCount, getChatHistoryFilePath, displayChatHistory } from '../src/chat-history.js';
18
18
  import { installShellHook, uninstallShellHook, getHookStatus, detectShell, getShellConfigPath, displayShellHistory, clearShellHistory, } from '../src/shell-hook.js';
@@ -20,8 +20,11 @@ import { checkForUpdates, showUpdateNotice, performUpgrade, } from '../src/upgra
20
20
  import { getCurrentTheme } from '../src/ui/theme.js';
21
21
  import { addAlias, removeAlias, displayAliases, resolveAlias, } from '../src/alias.js';
22
22
  import { addRemote, removeRemote, displayRemotes, getRemote, testRemoteConnection, sshExec, collectRemoteSysInfo, setRemoteWorkDir, getRemoteWorkDir, generateBatchRemoteCommands, executeBatchRemoteCommands, } from '../src/remote.js';
23
+ import { getSystemInfo, refreshSystemCache, displaySystemInfo } from '../src/sysinfo.js';
24
+ import { displayCommandStats, clearCommandStats, getStatsFilePath, } from '../src/user-preferences.js';
23
25
  import { addRemoteHistory, displayRemoteHistory, clearRemoteHistory, fetchRemoteShellHistory, displayRemoteShellHistory, clearRemoteShellHistory, } from '../src/remote-history.js';
24
26
  import { detectRemoteShell, getRemoteShellConfigPath, installRemoteShellHook, uninstallRemoteShellHook, getRemoteHookStatus, } from '../src/shell-hook.js';
27
+ import { buildShellExecConfig, } from '../src/utils/platform.js';
25
28
  // 获取主题颜色的辅助函数
26
29
  function getThemeColors() {
27
30
  const theme = getCurrentTheme();
@@ -65,10 +68,81 @@ process.on('beforeExit', () => {
65
68
  showUpdateNotice(packageJson.version, updateCheckResult.latestVersion);
66
69
  }
67
70
  });
71
+ /**
72
+ * 需要 TTY 的工具白名单
73
+ * 这些工具在 pipe 模式下可能会卡住或无输出
74
+ */
75
+ const TTY_REQUIRED_COMMANDS = new Set([
76
+ // ls 替代品(带图标/颜色)
77
+ 'eza', 'exa', 'lsd',
78
+ // cat 替代品(带语法高亮)
79
+ 'bat', 'batcat',
80
+ // diff 替代品
81
+ 'delta', 'diff-so-fancy',
82
+ // 系统监控
83
+ 'htop', 'btop', 'top', 'glances', 'gtop', 'bpytop',
84
+ // 编辑器
85
+ 'vim', 'nvim', 'nano', 'emacs', 'micro', 'helix', 'hx',
86
+ // 分页器
87
+ 'less', 'more', 'most',
88
+ // 模糊搜索
89
+ 'fzf', 'skim', 'sk',
90
+ // 终端复用器
91
+ 'tmux', 'screen', 'zellij',
92
+ // TUI 工具
93
+ 'lazygit', 'lazydocker', 'lazysql', 'k9s', 'tig',
94
+ // 文件管理器
95
+ 'nnn', 'ranger', 'lf', 'yazi', 'mc', 'vifm',
96
+ // 数据查看
97
+ 'visidata', 'vd',
98
+ ]);
99
+ /**
100
+ * 使用 inherit 模式执行命令(用于需要 TTY 的工具)
101
+ * 特点:命令能正常执行,但无法捕获输出
102
+ */
103
+ function executeWithInherit(command) {
104
+ return new Promise((resolve) => {
105
+ console.log(''); // 空行
106
+ // 计算命令框宽度
107
+ const termWidth = process.stdout.columns || 80;
108
+ const maxContentWidth = termWidth - 6;
109
+ const lines = command.split('\n');
110
+ const wrappedLines = [];
111
+ for (const line of lines) {
112
+ wrappedLines.push(...console2.wrapText(line, maxContentWidth));
113
+ }
114
+ const actualMaxWidth = Math.max(...wrappedLines.map((l) => console2.getDisplayWidth(l)), console2.getDisplayWidth('生成命令'));
115
+ const boxWidth = Math.max(console2.MIN_COMMAND_BOX_WIDTH, Math.min(actualMaxWidth + 4, termWidth - 2));
116
+ console2.printSeparator('输出', boxWidth);
117
+ // 使用 platform 模块构建跨平台命令执行配置
118
+ const execConfig = buildShellExecConfig(command);
119
+ // 使用 spawn + inherit(输出直接到终端)
120
+ const child = spawn(execConfig.shell, execConfig.args, {
121
+ stdio: 'inherit',
122
+ env: process.env,
123
+ });
124
+ child.on('close', (code) => {
125
+ console2.printSeparator('', boxWidth);
126
+ resolve({ exitCode: code || 0, output: '', stdout: '', stderr: '' });
127
+ });
128
+ child.on('error', (err) => {
129
+ console2.printSeparator('', boxWidth);
130
+ console2.error(err.message);
131
+ resolve({ exitCode: 1, output: err.message, stdout: '', stderr: err.message });
132
+ });
133
+ });
134
+ }
68
135
  /**
69
136
  * 执行命令(原生版本)
70
137
  */
71
138
  function executeCommand(command) {
139
+ // 检测是否是需要 TTY 的工具
140
+ const firstCmd = command.trim().split(/[\s|&;]/)[0];
141
+ if (TTY_REQUIRED_COMMANDS.has(firstCmd)) {
142
+ // 使用 inherit 模式执行(无法捕获输出,但能正常运行)
143
+ return executeWithInherit(command);
144
+ }
145
+ // 普通命令:使用 pipe 模式(捕获输出)
72
146
  return new Promise((resolve) => {
73
147
  let stdout = '';
74
148
  let stderr = '';
@@ -85,8 +159,9 @@ function executeCommand(command) {
85
159
  const actualMaxWidth = Math.max(...wrappedLines.map((l) => console2.getDisplayWidth(l)), console2.getDisplayWidth('生成命令'));
86
160
  const boxWidth = Math.max(console2.MIN_COMMAND_BOX_WIDTH, Math.min(actualMaxWidth + 4, termWidth - 2));
87
161
  console2.printSeparator('输出', boxWidth);
88
- // 使用 bash 并启用 pipefail,确保管道中任何命令失败都能正确返回非零退出码
89
- const child = exec(`set -o pipefail; ${command}`, { shell: '/bin/bash' });
162
+ // 使用 platform 模块构建跨平台命令执行配置
163
+ const execConfig = buildShellExecConfig(command);
164
+ const child = exec(execConfig.command, { shell: execConfig.shell });
90
165
  child.stdout?.on('data', (data) => {
91
166
  stdout += data;
92
167
  hasOutput = true;
@@ -127,24 +202,7 @@ configCmd
127
202
  .alias('show')
128
203
  .description('查看当前配置')
129
204
  .action(() => {
130
- const config = getConfig();
131
- const CONFIG_FILE = join(os.homedir(), '.please', 'config.json');
132
- console.log('');
133
- console2.title('当前配置:');
134
- console2.muted('━'.repeat(50));
135
- console.log(` ${chalk.hex(getThemeColors().primary)('apiKey')}: ${maskApiKey(config.apiKey)}`);
136
- console.log(` ${chalk.hex(getThemeColors().primary)('baseUrl')}: ${config.baseUrl}`);
137
- console.log(` ${chalk.hex(getThemeColors().primary)('provider')}: ${config.provider}`);
138
- console.log(` ${chalk.hex(getThemeColors().primary)('model')}: ${config.model}`);
139
- console.log(` ${chalk.hex(getThemeColors().primary)('shellHook')}: ${config.shellHook ? chalk.hex(getThemeColors().success)('已启用') : chalk.gray('未启用')}`);
140
- console.log(` ${chalk.hex(getThemeColors().primary)('editMode')}: ${config.editMode === 'auto' ? chalk.hex(getThemeColors().primary)('auto (自动编辑)') : chalk.gray('manual (按E编辑)')}`);
141
- console.log(` ${chalk.hex(getThemeColors().primary)('chatHistoryLimit')}: ${config.chatHistoryLimit} 轮`);
142
- console.log(` ${chalk.hex(getThemeColors().primary)('commandHistoryLimit')}: ${config.commandHistoryLimit} 条`);
143
- console.log(` ${chalk.hex(getThemeColors().primary)('shellHistoryLimit')}: ${config.shellHistoryLimit} 条`);
144
- console.log(` ${chalk.hex(getThemeColors().primary)('theme')}: ${config.theme === 'dark' ? chalk.hex(getThemeColors().primary)('dark (深色)') : chalk.hex(getThemeColors().primary)('light (浅色)')}`);
145
- console2.muted('━'.repeat(50));
146
- console2.muted(`配置文件: ${CONFIG_FILE}`);
147
- console.log('');
205
+ displayConfig();
148
206
  });
149
207
  configCmd
150
208
  .command('set <key> <value>')
@@ -648,6 +706,51 @@ aliasCmd
648
706
  aliasCmd.action(() => {
649
707
  displayAliases();
650
708
  });
709
+ // sysinfo 子命令
710
+ const sysinfoCmd = program.command('sysinfo').description('管理系统信息');
711
+ sysinfoCmd
712
+ .command('show')
713
+ .description('查看系统信息')
714
+ .action(async () => {
715
+ const info = await getSystemInfo();
716
+ displaySystemInfo(info);
717
+ });
718
+ sysinfoCmd
719
+ .command('refresh')
720
+ .description('刷新系统信息缓存')
721
+ .action(() => {
722
+ console.log('');
723
+ refreshSystemCache();
724
+ console.log('');
725
+ });
726
+ // 默认 sysinfo 命令(显示信息)
727
+ sysinfoCmd.action(async () => {
728
+ const info = await getSystemInfo();
729
+ displaySystemInfo(info);
730
+ });
731
+ // prefs 子命令
732
+ const prefsCmd = program.command('prefs').description('管理命令偏好统计');
733
+ prefsCmd
734
+ .command('show')
735
+ .description('查看命令偏好统计')
736
+ .action(() => {
737
+ displayCommandStats();
738
+ });
739
+ prefsCmd
740
+ .command('clear')
741
+ .description('清空偏好统计')
742
+ .action(() => {
743
+ const colors = getThemeColors();
744
+ clearCommandStats();
745
+ console.log('');
746
+ console.log(chalk.hex(colors.success)('✓ 已清空命令偏好统计'));
747
+ console.log(chalk.gray(` 统计文件: ${getStatsFilePath()}`));
748
+ console.log('');
749
+ });
750
+ // 默认 prefs 命令(显示统计)
751
+ prefsCmd.action(() => {
752
+ displayCommandStats();
753
+ });
651
754
  // remote 子命令
652
755
  const remoteCmd = program.command('remote').description('管理远程服务器');
653
756
  remoteCmd
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yivan-lab/pretty-please",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "AI 驱动的命令行工具,将自然语言转换为可执行的 Shell 命令",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,6 +11,10 @@
11
11
  "dev": "tsx bin/pls.tsx",
12
12
  "build": "tsc && node scripts/postbuild.js",
13
13
  "start": "node dist/bin/pls.js",
14
+ "test": "vitest run",
15
+ "test:watch": "vitest",
16
+ "test:ui": "vitest --ui",
17
+ "test:coverage": "vitest run --coverage",
14
18
  "link:dev": "mkdir -p ~/.local/bin && ln -sf \"$(pwd)/bin/pls.tsx\" ~/.local/bin/pls-dev && echo '✅ pls-dev 已链接到 ~/.local/bin/pls-dev'",
15
19
  "unlink:dev": "rm -f ~/.local/bin/pls-dev && echo '✅ pls-dev 已移除'",
16
20
  "prepublishOnly": "pnpm build"
@@ -55,6 +59,7 @@
55
59
  "chalk": "^5.6.2",
56
60
  "cli-highlight": "^2.1.11",
57
61
  "commander": "^14.0.2",
62
+ "detect-package-manager": "^3.0.2",
58
63
  "ink": "^6.5.1",
59
64
  "ink-box": "^2.0.0",
60
65
  "ink-markdown": "^1.0.4",
@@ -73,8 +78,11 @@
73
78
  "@types/hast": "^3.0.4",
74
79
  "@types/node": "^25.0.2",
75
80
  "@types/react": "^19.2.7",
81
+ "@vitest/coverage-v8": "^4.0.16",
82
+ "@vitest/ui": "^4.0.16",
76
83
  "react-devtools-core": "^7.0.1",
77
84
  "tsx": "^4.21.0",
78
- "typescript": "^5.9.3"
85
+ "typescript": "^5.9.3",
86
+ "vitest": "^4.0.16"
79
87
  }
80
88
  }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * 命令生成工作流集成测试
3
+ * 测试用户输入 → AI生成 → 确认 → 执行 → 成功/失败 的完整流程
4
+ */
5
+ export {};